@sweny-ai/core 0.1.76 → 0.1.78

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.
Files changed (123) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +20 -0
  4. package/dist/claude.js +139 -9
  5. package/dist/cli/config.d.ts +22 -0
  6. package/dist/cli/config.js +56 -9
  7. package/dist/cli/main.d.ts +1 -1
  8. package/dist/cli/main.js +56 -57
  9. package/dist/cli/marketplace.js +35 -9
  10. package/dist/cli/publish.d.ts +10 -0
  11. package/dist/cli/publish.js +22 -14
  12. package/dist/executor.js +104 -43
  13. package/dist/loader.d.ts +40 -0
  14. package/dist/loader.js +73 -0
  15. package/dist/mcp-catalog.d.ts +47 -0
  16. package/dist/mcp-catalog.js +304 -0
  17. package/dist/mcp.d.ts +6 -9
  18. package/dist/mcp.js +24 -314
  19. package/dist/requires.d.ts +7 -0
  20. package/dist/requires.js +34 -0
  21. package/dist/retry.d.ts +23 -0
  22. package/dist/retry.js +72 -0
  23. package/dist/schema.d.ts +667 -80
  24. package/dist/schema.js +144 -5
  25. package/dist/skills/custom-loader.d.ts +40 -0
  26. package/dist/skills/custom-loader.js +107 -32
  27. package/dist/skills/index.d.ts +13 -1
  28. package/dist/skills/index.js +12 -3
  29. package/dist/studio.js +4 -1
  30. package/dist/templates.d.ts +29 -8
  31. package/dist/templates.js +30 -20
  32. package/dist/testing.d.ts +7 -0
  33. package/dist/testing.js +7 -0
  34. package/dist/types.d.ts +76 -0
  35. package/dist/verify.d.ts +1 -0
  36. package/dist/verify.js +43 -5
  37. package/package.json +8 -4
  38. package/dist/__tests__/cascade.test.d.ts +0 -11
  39. package/dist/__tests__/cascade.test.js +0 -449
  40. package/dist/__tests__/claude.test.d.ts +0 -1
  41. package/dist/__tests__/claude.test.js +0 -427
  42. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  43. package/dist/__tests__/custom-loader.test.js +0 -116
  44. package/dist/__tests__/executor.test.d.ts +0 -1
  45. package/dist/__tests__/executor.test.js +0 -736
  46. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  48. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  50. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/github.integration.test.js +0 -37
  52. package/dist/__tests__/integration/harness.d.ts +0 -24
  53. package/dist/__tests__/integration/harness.js +0 -34
  54. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  55. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  56. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  57. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  58. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  59. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  60. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  61. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  62. package/dist/__tests__/publish.test.d.ts +0 -1
  63. package/dist/__tests__/publish.test.js +0 -180
  64. package/dist/__tests__/schema.test.d.ts +0 -1
  65. package/dist/__tests__/schema.test.js +0 -526
  66. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  67. package/dist/__tests__/skill-config-integration.test.js +0 -204
  68. package/dist/__tests__/skills-index.test.d.ts +0 -1
  69. package/dist/__tests__/skills-index.test.js +0 -119
  70. package/dist/__tests__/skills.test.d.ts +0 -1
  71. package/dist/__tests__/skills.test.js +0 -565
  72. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  73. package/dist/__tests__/spec-conformance.test.js +0 -1074
  74. package/dist/__tests__/studio.test.d.ts +0 -1
  75. package/dist/__tests__/studio.test.js +0 -403
  76. package/dist/__tests__/templates.test.d.ts +0 -1
  77. package/dist/__tests__/templates.test.js +0 -49
  78. package/dist/__tests__/testing.test.d.ts +0 -1
  79. package/dist/__tests__/testing.test.js +0 -224
  80. package/dist/__tests__/verify.test.d.ts +0 -1
  81. package/dist/__tests__/verify.test.js +0 -517
  82. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  83. package/dist/__tests__/version-consistency.test.js +0 -312
  84. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  85. package/dist/__tests__/workflow-builder.test.js +0 -110
  86. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  87. package/dist/__tests__/workflow-yaml.test.js +0 -201
  88. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  89. package/dist/cli/__tests__/diagram.test.js +0 -225
  90. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  91. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  92. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  93. package/dist/cli/__tests__/upgrade.test.js +0 -317
  94. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  95. package/dist/cli/__tests__/version-check.test.js +0 -209
  96. package/dist/cli/config.test.d.ts +0 -1
  97. package/dist/cli/config.test.js +0 -200
  98. package/dist/cli/credentials.test.d.ts +0 -1
  99. package/dist/cli/credentials.test.js +0 -172
  100. package/dist/cli/e2e.test.d.ts +0 -1
  101. package/dist/cli/e2e.test.js +0 -690
  102. package/dist/cli/marketplace.test.d.ts +0 -1
  103. package/dist/cli/marketplace.test.js +0 -355
  104. package/dist/cli/new.test.d.ts +0 -1
  105. package/dist/cli/new.test.js +0 -1043
  106. package/dist/cli/output.test.d.ts +0 -1
  107. package/dist/cli/output.test.js +0 -180
  108. package/dist/cli/publish.test.d.ts +0 -1
  109. package/dist/cli/publish.test.js +0 -610
  110. package/dist/cli/renderer.test.d.ts +0 -1
  111. package/dist/cli/renderer.test.js +0 -302
  112. package/dist/executor.test.d.ts +0 -1
  113. package/dist/executor.test.js +0 -603
  114. package/dist/mcp.test.d.ts +0 -1
  115. package/dist/mcp.test.js +0 -735
  116. package/dist/mermaid.test.d.ts +0 -1
  117. package/dist/mermaid.test.js +0 -256
  118. package/dist/sources.test.d.ts +0 -1
  119. package/dist/sources.test.js +0 -265
  120. package/dist/sources.validation.test.d.ts +0 -12
  121. package/dist/sources.validation.test.js +0 -380
  122. package/dist/workflow-builder.test.d.ts +0 -1
  123. package/dist/workflow-builder.test.js +0 -117
@@ -1,1043 +0,0 @@
1
- import { describe, it, expect, afterEach, vi } 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, collectCredentialsForSkills, extractSkillsFromYaml, detectGitRemote, buildSwenyYml, buildEnvTemplate, buildActionWorkflow, runNew, PROVIDER_CREDENTIALS, SKILL_CREDENTIALS, writeSwenyYmlIfMissing, appendMissingEnvKeys, writeWorkflowFile, } from "./new.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("github", "datadog", "linear");
175
- expect(yml).toContain("source-control-provider: github");
176
- expect(yml).toContain("observability-provider: datadog");
177
- expect(yml).toContain("issue-tracker-provider: linear");
178
- });
179
- it("omits observability-provider when null", () => {
180
- const yml = buildSwenyYml("github", null, "github-issues");
181
- expect(yml).not.toContain("observability-provider");
182
- });
183
- it("includes header comments", () => {
184
- const yml = buildSwenyYml("github", null, "github-issues");
185
- expect(yml).toContain("# .sweny.yml");
186
- });
187
- });
188
- // ── buildEnvTemplate ───────────────────────────────────────────────────
189
- describe("buildEnvTemplate", () => {
190
- it("includes url and hint comments", () => {
191
- const creds = collectCredentials({
192
- sourceControl: "github",
193
- observability: null,
194
- issueTracker: "github-issues",
195
- notification: "console",
196
- githubAction: false,
197
- cronExpression: null,
198
- });
199
- const template = buildEnvTemplate(creds);
200
- expect(template).toContain("# https://console.anthropic.com/settings/api-keys");
201
- expect(template).toContain("# Claude API key");
202
- expect(template).toContain("ANTHROPIC_API_KEY=");
203
- expect(template).toContain("# https://github.com/settings/tokens");
204
- expect(template).toContain("# repo + issues scopes");
205
- expect(template).toContain("GITHUB_TOKEN=");
206
- });
207
- it("pre-fills defaults", () => {
208
- const creds = collectCredentials({
209
- sourceControl: "gitlab",
210
- observability: "datadog",
211
- issueTracker: "jira",
212
- notification: "console",
213
- githubAction: false,
214
- cronExpression: null,
215
- });
216
- const template = buildEnvTemplate(creds);
217
- expect(template).toContain("GITLAB_URL=https://gitlab.com");
218
- expect(template).toContain("DD_SITE=datadoghq.com");
219
- });
220
- it("includes header comment", () => {
221
- const template = buildEnvTemplate([]);
222
- expect(template).toContain("# .env");
223
- });
224
- });
225
- // ── buildActionWorkflow ────────────────────────────────────────────────
226
- describe("buildActionWorkflow", () => {
227
- it("generates a valid workflow with weekly cron", () => {
228
- const creds = collectCredentials({
229
- sourceControl: "github",
230
- observability: null,
231
- issueTracker: "github-issues",
232
- notification: "console",
233
- githubAction: true,
234
- cronExpression: "0 8 * * 1",
235
- });
236
- const workflow = buildActionWorkflow(creds, "0 8 * * 1");
237
- expect(workflow).toContain("name: SWEny Triage");
238
- expect(workflow).toContain('cron: "0 8 * * 1"');
239
- expect(workflow).toContain("workflow_dispatch:");
240
- expect(workflow).toContain("actions/checkout@v4");
241
- expect(workflow).toContain("swenyai/sweny@v5");
242
- });
243
- it("generates a workflow with daily cron", () => {
244
- const creds = collectCredentials({
245
- sourceControl: "github",
246
- observability: "sentry",
247
- issueTracker: "linear",
248
- notification: "slack",
249
- githubAction: true,
250
- cronExpression: "0 9 * * *",
251
- });
252
- const workflow = buildActionWorkflow(creds, "0 9 * * *");
253
- expect(workflow).toContain('cron: "0 9 * * *"');
254
- });
255
- it("skips credentials with defaults from secrets", () => {
256
- const creds = collectCredentials({
257
- sourceControl: "github",
258
- observability: "datadog",
259
- issueTracker: "github-issues",
260
- notification: "console",
261
- githubAction: true,
262
- cronExpression: "0 8 * * 1",
263
- });
264
- const workflow = buildActionWorkflow(creds, "0 8 * * 1");
265
- // DD_SITE has a default, so it should NOT appear in secrets
266
- expect(workflow).not.toContain("secrets.DD_SITE");
267
- // DD_API_KEY has no default, so it should appear
268
- expect(workflow).toContain("secrets.DD_API_KEY");
269
- expect(workflow).toContain("secrets.DD_APP_KEY");
270
- expect(workflow).toContain("secrets.ANTHROPIC_API_KEY");
271
- expect(workflow).toContain("secrets.GITHUB_TOKEN");
272
- });
273
- it("maps credentials to ${{ secrets.KEY }} format", () => {
274
- const creds = collectCredentials({
275
- sourceControl: "github",
276
- observability: null,
277
- issueTracker: "linear",
278
- notification: "slack",
279
- githubAction: true,
280
- cronExpression: "0 8 * * 1",
281
- });
282
- const workflow = buildActionWorkflow(creds, "0 8 * * 1");
283
- expect(workflow).toContain("ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}");
284
- expect(workflow).toContain("GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}");
285
- expect(workflow).toContain("LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}");
286
- expect(workflow).toContain("SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}");
287
- });
288
- });
289
- // ═══════════════════════════════════════════════════════════════════════
290
- // Additional tests: edge cases, correctness, security
291
- // ═══════════════════════════════════════════════════════════════════════
292
- // ── detectGitRemote: additional edge cases ────────────────────────────
293
- describe("detectGitRemote — edge cases", () => {
294
- const tmpDirs = [];
295
- function makeTempRepo(gitConfigContent) {
296
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-init-test-"));
297
- tmpDirs.push(dir);
298
- const gitDir = path.join(dir, ".git");
299
- fs.mkdirSync(gitDir, { recursive: true });
300
- fs.writeFileSync(path.join(gitDir, "config"), gitConfigContent);
301
- return dir;
302
- }
303
- afterEach(() => {
304
- for (const d of tmpDirs) {
305
- fs.rmSync(d, { recursive: true, force: true });
306
- }
307
- tmpDirs.length = 0;
308
- });
309
- it("detects ssh:// scheme GitHub remote", () => {
310
- const cwd = makeTempRepo(`
311
- [remote "origin"]
312
- \turl = ssh://git@github.com/acme/widgets.git
313
- `);
314
- const info = detectGitRemote(cwd);
315
- expect(info).toEqual({
316
- provider: "github",
317
- remote: "github.com/acme/widgets",
318
- });
319
- });
320
- it("detects ssh:// scheme GitLab remote", () => {
321
- const cwd = makeTempRepo(`
322
- [remote "origin"]
323
- \turl = ssh://git@gitlab.com/team/project.git
324
- `);
325
- const info = detectGitRemote(cwd);
326
- expect(info).toEqual({
327
- provider: "gitlab",
328
- remote: "gitlab.com/team/project",
329
- });
330
- });
331
- it("returns null for ssh:// with unknown host", () => {
332
- const cwd = makeTempRepo(`
333
- [remote "origin"]
334
- \turl = ssh://git@bitbucket.org/acme/widgets.git
335
- `);
336
- expect(detectGitRemote(cwd)).toBeNull();
337
- });
338
- it("handles git config with multiple remotes (uses origin)", () => {
339
- const cwd = makeTempRepo(`
340
- [remote "upstream"]
341
- \turl = https://gitlab.com/upstream/project.git
342
- [remote "origin"]
343
- \turl = https://github.com/acme/widgets.git
344
- `);
345
- const info = detectGitRemote(cwd);
346
- expect(info).toEqual({
347
- provider: "github",
348
- remote: "github.com/acme/widgets",
349
- });
350
- });
351
- it("handles nested org paths (gitlab subgroups)", () => {
352
- const cwd = makeTempRepo(`
353
- [remote "origin"]
354
- \turl = https://gitlab.com/org/team/subteam/project.git
355
- `);
356
- const info = detectGitRemote(cwd);
357
- expect(info).toEqual({
358
- provider: "gitlab",
359
- remote: "gitlab.com/org/team/subteam/project",
360
- });
361
- });
362
- it("handles empty git config file", () => {
363
- const cwd = makeTempRepo("");
364
- expect(detectGitRemote(cwd)).toBeNull();
365
- });
366
- it("handles git config with no origin remote", () => {
367
- const cwd = makeTempRepo(`
368
- [remote "upstream"]
369
- \turl = https://github.com/acme/widgets.git
370
- `);
371
- expect(detectGitRemote(cwd)).toBeNull();
372
- });
373
- });
374
- // ── buildSwenyYml: YAML validity ──────────────────────────────────────
375
- describe("buildSwenyYml — YAML validity", () => {
376
- it("generates valid YAML that can be parsed", () => {
377
- const yml = buildSwenyYml("github", "datadog", "linear");
378
- const parsed = parseYaml(yml);
379
- expect(parsed["source-control-provider"]).toBe("github");
380
- expect(parsed["observability-provider"]).toBe("datadog");
381
- expect(parsed["issue-tracker-provider"]).toBe("linear");
382
- });
383
- it("generates valid YAML with minimal selections", () => {
384
- const yml = buildSwenyYml("github", null, "github-issues");
385
- const parsed = parseYaml(yml);
386
- expect(parsed["source-control-provider"]).toBe("github");
387
- expect(parsed["issue-tracker-provider"]).toBe("github-issues");
388
- expect(parsed["observability-provider"]).toBeUndefined();
389
- });
390
- it("uses correct key names matching config parser expectations", () => {
391
- const yml = buildSwenyYml("gitlab", "sentry", "jira");
392
- // These are the exact keys the config parser reads
393
- expect(yml).toContain("source-control-provider:");
394
- expect(yml).toContain("observability-provider:");
395
- expect(yml).toContain("issue-tracker-provider:");
396
- // Must NOT contain the short form
397
- expect(yml).not.toMatch(/^source-control:/m);
398
- expect(yml).not.toMatch(/^issue-tracker:/m);
399
- });
400
- it("every provider combination produces valid YAML", () => {
401
- const sourceControls = ["github", "gitlab"];
402
- const observabilities = ["datadog", "sentry", "betterstack", "newrelic", "cloudwatch", null];
403
- const issueTrackers = ["github-issues", "linear", "jira"];
404
- for (const sc of sourceControls) {
405
- for (const obs of observabilities) {
406
- for (const it of issueTrackers) {
407
- const yml = buildSwenyYml(sc, obs, it);
408
- // Should not throw
409
- const parsed = parseYaml(yml);
410
- expect(parsed["source-control-provider"]).toBe(sc);
411
- }
412
- }
413
- }
414
- });
415
- });
416
- // ── buildEnvTemplate: correctness ─────────────────────────────────────
417
- describe("buildEnvTemplate — correctness", () => {
418
- it("every credential has exactly one KEY= line", () => {
419
- const creds = collectCredentials({
420
- sourceControl: "github",
421
- observability: "datadog",
422
- issueTracker: "linear",
423
- notification: "slack",
424
- githubAction: false,
425
- cronExpression: null,
426
- });
427
- const template = buildEnvTemplate(creds);
428
- for (const cred of creds) {
429
- const matches = template.match(new RegExp(`^${cred.key}=`, "gm"));
430
- expect(matches, `${cred.key} should appear exactly once`).toHaveLength(1);
431
- }
432
- });
433
- it("does not include empty KEY= lines for credentials with defaults", () => {
434
- const creds = collectCredentials({
435
- sourceControl: "github",
436
- observability: "datadog",
437
- issueTracker: "github-issues",
438
- notification: "console",
439
- githubAction: false,
440
- cronExpression: null,
441
- });
442
- const template = buildEnvTemplate(creds);
443
- // DD_SITE has a default — should be pre-filled, not empty
444
- expect(template).toContain("DD_SITE=datadoghq.com");
445
- expect(template).not.toMatch(/^DD_SITE=$/m);
446
- });
447
- it("header warns about not committing", () => {
448
- const template = buildEnvTemplate([]);
449
- expect(template).toContain("DO NOT COMMIT");
450
- });
451
- it("header mentions sweny check", () => {
452
- const template = buildEnvTemplate([]);
453
- expect(template).toContain("sweny check");
454
- });
455
- });
456
- // ── buildActionWorkflow: YAML validity ────────────────────────────────
457
- describe("buildActionWorkflow — YAML validity", () => {
458
- it("generates valid GitHub Actions YAML", () => {
459
- const creds = collectCredentials({
460
- sourceControl: "github",
461
- observability: "datadog",
462
- issueTracker: "linear",
463
- notification: "console",
464
- githubAction: true,
465
- cronExpression: "0 9 * * 1",
466
- });
467
- const workflow = buildActionWorkflow(creds, "0 9 * * 1");
468
- const parsed = parseYaml(workflow);
469
- expect(parsed.name).toBe("SWEny Triage");
470
- expect(parsed.on.schedule[0].cron).toBe("0 9 * * 1");
471
- expect(parsed.on.workflow_dispatch).toBeDefined();
472
- expect(parsed.jobs.triage["runs-on"]).toBe("ubuntu-latest");
473
- });
474
- it("includes with.workflow: triage", () => {
475
- const creds = collectCredentials({
476
- sourceControl: "github",
477
- observability: null,
478
- issueTracker: "github-issues",
479
- notification: "console",
480
- githubAction: true,
481
- cronExpression: "0 9 * * 1",
482
- });
483
- const workflow = buildActionWorkflow(creds, "0 9 * * 1");
484
- expect(workflow).toContain("workflow: triage");
485
- });
486
- it("omits env block when all credentials have defaults", () => {
487
- // Only credentials with defaults — should produce no env block
488
- const workflow = buildActionWorkflow([
489
- { key: "DD_SITE", default: "datadoghq.com" },
490
- { key: "AWS_REGION", default: "us-east-1" },
491
- ], "0 9 * * 1");
492
- expect(workflow).not.toContain("env:");
493
- });
494
- it("generates valid YAML for every provider combination", () => {
495
- const combos = [
496
- {
497
- sourceControl: "github",
498
- observability: "datadog",
499
- issueTracker: "linear",
500
- notification: "slack",
501
- githubAction: true,
502
- cronExpression: "0 9 * * 1",
503
- },
504
- {
505
- sourceControl: "gitlab",
506
- observability: "sentry",
507
- issueTracker: "jira",
508
- notification: "teams",
509
- githubAction: true,
510
- cronExpression: "0 9 * * *",
511
- },
512
- {
513
- sourceControl: "github",
514
- observability: "cloudwatch",
515
- issueTracker: "github-issues",
516
- notification: "discord",
517
- githubAction: true,
518
- cronExpression: "0 8 * * 1-5",
519
- },
520
- {
521
- sourceControl: "github",
522
- observability: null,
523
- issueTracker: "github-issues",
524
- notification: "console",
525
- githubAction: true,
526
- cronExpression: "0 9 * * 1",
527
- },
528
- ];
529
- for (const sel of combos) {
530
- const creds = collectCredentials(sel);
531
- const workflow = buildActionWorkflow(creds, sel.cronExpression);
532
- // Should not throw
533
- const parsed = parseYaml(workflow);
534
- expect(parsed.name).toBe("SWEny Triage");
535
- expect(parsed.jobs.triage).toBeDefined();
536
- }
537
- });
538
- });
539
- // ── collectCredentials: additional cases ──────────────────────────────
540
- describe("collectCredentials — additional cases", () => {
541
- it("ANTHROPIC_API_KEY is always first", () => {
542
- const creds = collectCredentials({
543
- sourceControl: "github",
544
- observability: "datadog",
545
- issueTracker: "linear",
546
- notification: "slack",
547
- githubAction: false,
548
- cronExpression: null,
549
- });
550
- expect(creds[0].key).toBe("ANTHROPIC_API_KEY");
551
- });
552
- it("ANTHROPIC_API_KEY has correct url", () => {
553
- const creds = collectCredentials({
554
- sourceControl: "github",
555
- observability: null,
556
- issueTracker: "github-issues",
557
- notification: "console",
558
- githubAction: false,
559
- cronExpression: null,
560
- });
561
- const anthropic = creds.find((c) => c.key === "ANTHROPIC_API_KEY");
562
- expect(anthropic?.url).toBe("https://console.anthropic.com/settings/api-keys");
563
- });
564
- it("handles unknown provider gracefully (custom observability)", () => {
565
- const creds = collectCredentials({
566
- sourceControl: "github",
567
- observability: "splunk",
568
- issueTracker: "github-issues",
569
- notification: "console",
570
- githubAction: false,
571
- cronExpression: null,
572
- });
573
- // Should still have ANTHROPIC_API_KEY + GITHUB_TOKEN, no crash
574
- const keys = creds.map((c) => c.key);
575
- expect(keys).toContain("ANTHROPIC_API_KEY");
576
- expect(keys).toContain("GITHUB_TOKEN");
577
- });
578
- it("every provider in PROVIDER_CREDENTIALS has valid credential entries", () => {
579
- for (const [provider, creds] of Object.entries(PROVIDER_CREDENTIALS)) {
580
- expect(Array.isArray(creds), `${provider} should have an array`).toBe(true);
581
- for (const cred of creds) {
582
- expect(typeof cred.key, `${provider}.${cred.key} should be a string`).toBe("string");
583
- expect(cred.key.length, `${provider} has an empty key`).toBeGreaterThan(0);
584
- expect(cred.key, `${provider}.${cred.key} should be UPPER_SNAKE_CASE`).toMatch(/^[A-Z][A-Z0-9_]+$/);
585
- }
586
- }
587
- });
588
- it("credential URLs are valid https URLs when present", () => {
589
- for (const [provider, creds] of Object.entries(PROVIDER_CREDENTIALS)) {
590
- for (const cred of creds) {
591
- if (cred.url) {
592
- expect(cred.url, `${provider}.${cred.key} URL should start with https://`).toMatch(/^https:\/\//);
593
- }
594
- }
595
- }
596
- });
597
- });
598
- // ═══════════════════════════════════════════════════════════════════════
599
- // Workflow-first functions: extractSkillsFromYaml, collectCredentialsForSkills
600
- // ═══════════════════════════════════════════════════════════════════════
601
- describe("extractSkillsFromYaml", () => {
602
- it("extracts skills from a single node", () => {
603
- const yaml = `
604
- nodes:
605
- review:
606
- name: Code Review
607
- instruction: Review the PR
608
- skills: [github]
609
- `;
610
- expect(extractSkillsFromYaml(yaml)).toEqual(["github"]);
611
- });
612
- it("extracts and deduplicates skills from multiple nodes", () => {
613
- const yaml = `
614
- nodes:
615
- review:
616
- name: Code Review
617
- skills: [github, linear]
618
- scan:
619
- name: Security Scan
620
- skills: [github, sentry]
621
- `;
622
- const skills = extractSkillsFromYaml(yaml);
623
- expect(skills).toContain("github");
624
- expect(skills).toContain("linear");
625
- expect(skills).toContain("sentry");
626
- expect(skills.filter((s) => s === "github")).toHaveLength(1);
627
- });
628
- it("returns empty array when no skills present", () => {
629
- const yaml = `
630
- nodes:
631
- review:
632
- name: Code Review
633
- instruction: Review the PR
634
- `;
635
- expect(extractSkillsFromYaml(yaml)).toEqual([]);
636
- });
637
- it("handles skills with spaces around commas", () => {
638
- const yaml = `
639
- nodes:
640
- review:
641
- skills: [github , linear , sentry]
642
- `;
643
- const skills = extractSkillsFromYaml(yaml);
644
- expect(skills).toEqual(["github", "linear", "sentry"]);
645
- });
646
- it("handles single-skill arrays", () => {
647
- const yaml = `
648
- nodes:
649
- review:
650
- skills: [datadog]
651
- `;
652
- expect(extractSkillsFromYaml(yaml)).toEqual(["datadog"]);
653
- });
654
- });
655
- describe("collectCredentialsForSkills", () => {
656
- it("always includes ANTHROPIC_API_KEY even with no skills", () => {
657
- const creds = collectCredentialsForSkills([]);
658
- expect(creds.map((c) => c.key)).toEqual(["ANTHROPIC_API_KEY"]);
659
- });
660
- it("includes credentials for specified skills", () => {
661
- const creds = collectCredentialsForSkills(["github", "linear"]);
662
- const keys = creds.map((c) => c.key);
663
- expect(keys).toContain("ANTHROPIC_API_KEY");
664
- expect(keys).toContain("GITHUB_TOKEN");
665
- expect(keys).toContain("LINEAR_API_KEY");
666
- expect(keys).toContain("LINEAR_TEAM_ID");
667
- });
668
- it("deduplicates credentials across skills", () => {
669
- const creds = collectCredentialsForSkills(["github", "github"]);
670
- const tokenCount = creds.filter((c) => c.key === "GITHUB_TOKEN").length;
671
- expect(tokenCount).toBe(1);
672
- });
673
- it("skips unknown skills gracefully", () => {
674
- const creds = collectCredentialsForSkills(["github", "nonexistent-skill"]);
675
- const keys = creds.map((c) => c.key);
676
- expect(keys).toContain("ANTHROPIC_API_KEY");
677
- expect(keys).toContain("GITHUB_TOKEN");
678
- expect(keys).toHaveLength(2);
679
- });
680
- it("ANTHROPIC_API_KEY is always first", () => {
681
- const creds = collectCredentialsForSkills(["datadog", "github"]);
682
- expect(creds[0].key).toBe("ANTHROPIC_API_KEY");
683
- });
684
- it("derives credentials from skill.config when availableSkills provided", () => {
685
- const fakeSkill = {
686
- id: "my-tax-tool",
687
- name: "my-tax-tool",
688
- description: "Tax tool",
689
- category: "general",
690
- config: {
691
- MY_TAX_KEY: { description: "Tax API key", required: true, env: "MY_TAX_KEY" },
692
- MY_TAX_REGION: { description: "Region code", required: false, env: "MY_TAX_REGION" },
693
- },
694
- tools: [],
695
- };
696
- const creds = collectCredentialsForSkills(["my-tax-tool"], [fakeSkill]);
697
- const keys = creds.map((c) => c.key);
698
- expect(keys).toContain("ANTHROPIC_API_KEY");
699
- expect(keys).toContain("MY_TAX_KEY");
700
- expect(keys).toContain("MY_TAX_REGION");
701
- });
702
- it("falls back to SKILL_CREDENTIALS when skill not in availableSkills", () => {
703
- const creds = collectCredentialsForSkills(["github"], []);
704
- const keys = creds.map((c) => c.key);
705
- expect(keys).toContain("GITHUB_TOKEN");
706
- });
707
- it("uses SKILL_EXTRAS for url/hint when deriving from config", () => {
708
- const githubSkill = {
709
- id: "github",
710
- name: "github",
711
- description: "GitHub",
712
- category: "git",
713
- config: { GITHUB_TOKEN: { description: "GitHub token", required: true, env: "GITHUB_TOKEN" } },
714
- tools: [],
715
- };
716
- const creds = collectCredentialsForSkills(["github"], [githubSkill]);
717
- const token = creds.find((c) => c.key === "GITHUB_TOKEN");
718
- expect(token?.url).toBe("https://github.com/settings/tokens");
719
- });
720
- });
721
- // ── extractSkillsFromYaml: block-style arrays + malformed input ───────
722
- //
723
- // Regression: an earlier regex-based implementation only matched inline
724
- // arrays (`skills: [a, b]`). `yaml.stringify()` emits block style by
725
- // default, so AI-generated `__custom` workflows silently dropped every
726
- // skill and the wizard then asked for zero credentials. These tests pin
727
- // the parser-based implementation to handle both forms.
728
- describe("extractSkillsFromYaml — block-style arrays", () => {
729
- it("extracts skills from block-style arrays (yaml.stringify default)", () => {
730
- const yaml = `nodes:
731
- review:
732
- name: Code Review
733
- skills:
734
- - github
735
- - linear
736
- `;
737
- const skills = extractSkillsFromYaml(yaml);
738
- expect(skills.sort()).toEqual(["github", "linear"]);
739
- });
740
- it("deduplicates across block-style nodes", () => {
741
- const yaml = `nodes:
742
- first:
743
- skills:
744
- - github
745
- - sentry
746
- second:
747
- skills:
748
- - github
749
- - linear
750
- `;
751
- const skills = extractSkillsFromYaml(yaml).sort();
752
- expect(skills).toEqual(["github", "linear", "sentry"]);
753
- });
754
- it("handles mixed inline and block-style skills in the same doc", () => {
755
- const yaml = `nodes:
756
- a:
757
- skills: [github, datadog]
758
- b:
759
- skills:
760
- - linear
761
- - jira
762
- `;
763
- const skills = extractSkillsFromYaml(yaml).sort();
764
- expect(skills).toEqual(["datadog", "github", "jira", "linear"]);
765
- });
766
- it("ignores non-string skill entries defensively", () => {
767
- const yaml = `nodes:
768
- review:
769
- skills:
770
- - github
771
- - 42
772
- - null
773
- - ""
774
- `;
775
- // Only "github" survives the typeof-string + non-empty filter
776
- expect(extractSkillsFromYaml(yaml)).toEqual(["github"]);
777
- });
778
- it("returns empty array for malformed YAML (does not throw)", () => {
779
- const yaml = `nodes:
780
- review:
781
- skills: [github, linear
782
- # unclosed bracket above — invalid yaml
783
- `;
784
- expect(extractSkillsFromYaml(yaml)).toEqual([]);
785
- });
786
- it("returns empty array for YAML with no nodes key", () => {
787
- expect(extractSkillsFromYaml("foo: bar\n")).toEqual([]);
788
- });
789
- it("returns empty array for empty/whitespace input", () => {
790
- expect(extractSkillsFromYaml("")).toEqual([]);
791
- expect(extractSkillsFromYaml(" \n")).toEqual([]);
792
- });
793
- it("returns empty array when nodes entries have no skills field", () => {
794
- const yaml = `nodes:
795
- review:
796
- name: Code Review
797
- instruction: Review the PR
798
- `;
799
- expect(extractSkillsFromYaml(yaml)).toEqual([]);
800
- });
801
- });
802
- describe("SKILL_CREDENTIALS", () => {
803
- it("has entries for all built-in skills", () => {
804
- const expected = [
805
- "github",
806
- "gitlab",
807
- "datadog",
808
- "sentry",
809
- "betterstack",
810
- "newrelic",
811
- "linear",
812
- "jira",
813
- "slack",
814
- "discord",
815
- "notification",
816
- ];
817
- for (const skill of expected) {
818
- expect(SKILL_CREDENTIALS).toHaveProperty(skill);
819
- }
820
- });
821
- it("every skill has valid credential entries", () => {
822
- for (const [skill, creds] of Object.entries(SKILL_CREDENTIALS)) {
823
- expect(Array.isArray(creds), `${skill} should have an array`).toBe(true);
824
- for (const cred of creds) {
825
- expect(typeof cred.key, `${skill}.${cred.key} should be a string`).toBe("string");
826
- expect(cred.key, `${skill}.${cred.key} should be UPPER_SNAKE_CASE`).toMatch(/^[A-Z][A-Z0-9_]+$/);
827
- }
828
- }
829
- });
830
- });
831
- describe("writeSwenyYmlIfMissing", () => {
832
- const tmpDirs = [];
833
- afterEach(() => {
834
- for (const d of tmpDirs)
835
- fs.rmSync(d, { recursive: true, force: true });
836
- tmpDirs.length = 0;
837
- });
838
- it("creates .sweny.yml when missing", () => {
839
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
840
- tmpDirs.push(dir);
841
- const created = writeSwenyYmlIfMissing(dir, "github", "datadog", "github-issues");
842
- expect(created).toBe(true);
843
- expect(fs.existsSync(path.join(dir, ".sweny.yml"))).toBe(true);
844
- });
845
- it("does not overwrite existing .sweny.yml", () => {
846
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
847
- tmpDirs.push(dir);
848
- fs.writeFileSync(path.join(dir, ".sweny.yml"), "custom: value\n");
849
- const created = writeSwenyYmlIfMissing(dir, "github", null, "github-issues");
850
- expect(created).toBe(false);
851
- expect(fs.readFileSync(path.join(dir, ".sweny.yml"), "utf-8")).toBe("custom: value\n");
852
- });
853
- });
854
- describe("appendMissingEnvKeys", () => {
855
- const tmpDirs = [];
856
- afterEach(() => {
857
- for (const d of tmpDirs)
858
- fs.rmSync(d, { recursive: true, force: true });
859
- tmpDirs.length = 0;
860
- });
861
- it("creates .env from scratch", () => {
862
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
863
- tmpDirs.push(dir);
864
- const creds = [{ key: "FOO", hint: "set FOO" }];
865
- const added = appendMissingEnvKeys(dir, creds);
866
- expect(added).toBe(1);
867
- expect(fs.readFileSync(path.join(dir, ".env"), "utf-8")).toContain("FOO=");
868
- });
869
- it("skips already-present keys (including commented placeholders)", () => {
870
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
871
- tmpDirs.push(dir);
872
- fs.writeFileSync(path.join(dir, ".env"), "# FOO=set-me\nBAR=baz\n");
873
- const creds = [{ key: "FOO" }, { key: "BAR" }, { key: "QUUX" }];
874
- const added = appendMissingEnvKeys(dir, creds);
875
- expect(added).toBe(1);
876
- const final = fs.readFileSync(path.join(dir, ".env"), "utf-8");
877
- expect(final).toContain("QUUX=");
878
- });
879
- });
880
- describe("writeWorkflowFile", () => {
881
- const tmpDirs = [];
882
- afterEach(() => {
883
- for (const d of tmpDirs)
884
- fs.rmSync(d, { recursive: true, force: true });
885
- tmpDirs.length = 0;
886
- });
887
- it("writes a new workflow file", () => {
888
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
889
- tmpDirs.push(dir);
890
- const result = writeWorkflowFile(dir, "pr-review", "id: pr-review\n");
891
- expect(result).toEqual({ written: true, path: path.join(dir, ".sweny", "workflows", "pr-review.yml") });
892
- expect(fs.existsSync(result.path)).toBe(true);
893
- });
894
- it("returns exists=true when file already present and overwrite=false", () => {
895
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
896
- tmpDirs.push(dir);
897
- fs.mkdirSync(path.join(dir, ".sweny", "workflows"), { recursive: true });
898
- fs.writeFileSync(path.join(dir, ".sweny", "workflows", "pr-review.yml"), "old\n");
899
- const result = writeWorkflowFile(dir, "pr-review", "new\n", { overwrite: false });
900
- expect(result).toEqual({ written: false, exists: true, path: expect.any(String) });
901
- expect(fs.readFileSync(result.path, "utf-8")).toBe("old\n");
902
- });
903
- it("overwrites when overwrite=true", () => {
904
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
905
- tmpDirs.push(dir);
906
- fs.mkdirSync(path.join(dir, ".sweny", "workflows"), { recursive: true });
907
- fs.writeFileSync(path.join(dir, ".sweny", "workflows", "pr-review.yml"), "old\n");
908
- const result = writeWorkflowFile(dir, "pr-review", "new\n", { overwrite: true });
909
- expect(result).toEqual({ written: true, path: expect.any(String) });
910
- expect(fs.readFileSync(result.path, "utf-8")).toBe("new\n");
911
- });
912
- });
913
- vi.mock("./marketplace.js", async () => {
914
- const actual = await vi.importActual("./marketplace.js");
915
- return {
916
- ...actual,
917
- installMarketplaceWorkflow: vi.fn(),
918
- fetchMarketplaceIndex: vi.fn(),
919
- };
920
- });
921
- vi.mock("@clack/prompts", async () => {
922
- const actual = await vi.importActual("@clack/prompts");
923
- return {
924
- ...actual,
925
- intro: vi.fn(),
926
- outro: vi.fn(),
927
- cancel: vi.fn(),
928
- isCancel: vi.fn(() => false),
929
- select: vi.fn(),
930
- confirm: vi.fn(),
931
- text: vi.fn(),
932
- spinner: vi.fn(() => ({ start: vi.fn(), stop: vi.fn() })),
933
- log: {
934
- info: vi.fn(),
935
- success: vi.fn(),
936
- warn: vi.fn(),
937
- error: vi.fn(),
938
- message: vi.fn(),
939
- },
940
- note: vi.fn(),
941
- };
942
- });
943
- describe("runNew with marketplaceId", () => {
944
- const tmpDirs = [];
945
- afterEach(() => {
946
- for (const d of tmpDirs)
947
- fs.rmSync(d, { recursive: true, force: true });
948
- tmpDirs.length = 0;
949
- vi.restoreAllMocks();
950
- vi.clearAllMocks();
951
- });
952
- function tmp() {
953
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-new-mkt-"));
954
- tmpDirs.push(dir);
955
- return dir;
956
- }
957
- it("calls installMarketplaceWorkflow with pre-computed provider fields and overwrite:false when file absent", async () => {
958
- const cwd = tmp();
959
- vi.spyOn(process, "cwd").mockReturnValue(cwd);
960
- const mkt = await import("./marketplace.js");
961
- mkt.installMarketplaceWorkflow.mockResolvedValueOnce({
962
- installed: true,
963
- adapted: false,
964
- workflowPath: path.join(cwd, ".sweny", "workflows", "pr-review.yml"),
965
- mismatches: [],
966
- addedEnvKeys: 0,
967
- });
968
- await runNew({ marketplaceId: "pr-review" });
969
- expect(mkt.installMarketplaceWorkflow).toHaveBeenCalledWith("pr-review", expect.objectContaining({
970
- cwd,
971
- inferredSourceControl: expect.any(String),
972
- overwrite: false,
973
- }));
974
- // inferredIssueTracker / inferredObservability are skill-based — runNew
975
- // must NOT pre-compute them (it has no skills list), so marketplace.ts
976
- // can fall back to skill-based inference after fetching the workflow.
977
- const callArgs = mkt.installMarketplaceWorkflow.mock.calls[0][1];
978
- expect("inferredIssueTracker" in callArgs).toBe(false);
979
- expect("inferredObservability" in callArgs).toBe(false);
980
- });
981
- it("passes overwrite:true to installMarketplaceWorkflow when user confirms overwrite", async () => {
982
- const cwd = tmp();
983
- vi.spyOn(process, "cwd").mockReturnValue(cwd);
984
- // Pre-create the workflow file so the exists check triggers
985
- const wfDir = path.join(cwd, ".sweny", "workflows");
986
- fs.mkdirSync(wfDir, { recursive: true });
987
- fs.writeFileSync(path.join(wfDir, "pr-review.yml"), "# existing\n", "utf-8");
988
- const mkt = await import("./marketplace.js");
989
- const p = await import("@clack/prompts");
990
- mkt.installMarketplaceWorkflow.mockResolvedValueOnce({
991
- installed: true,
992
- adapted: false,
993
- workflowPath: path.join(wfDir, "pr-review.yml"),
994
- mismatches: [],
995
- addedEnvKeys: 0,
996
- });
997
- // User confirms overwrite
998
- p.confirm.mockResolvedValueOnce(true);
999
- await runNew({ marketplaceId: "pr-review" });
1000
- expect(p.confirm).toHaveBeenCalled();
1001
- expect(mkt.installMarketplaceWorkflow).toHaveBeenCalledWith("pr-review", expect.objectContaining({ overwrite: true }));
1002
- });
1003
- it("exits without installing when user declines overwrite for existing workflow", async () => {
1004
- const cwd = tmp();
1005
- vi.spyOn(process, "cwd").mockReturnValue(cwd);
1006
- // Pre-create the workflow file so the exists check triggers
1007
- const wfDir = path.join(cwd, ".sweny", "workflows");
1008
- fs.mkdirSync(wfDir, { recursive: true });
1009
- fs.writeFileSync(path.join(wfDir, "pr-review.yml"), "# existing\n", "utf-8");
1010
- const mkt = await import("./marketplace.js");
1011
- const p = await import("@clack/prompts");
1012
- // User declines overwrite
1013
- p.confirm.mockResolvedValueOnce(false);
1014
- const exitSpy = vi.spyOn(process, "exit").mockImplementation((_code) => {
1015
- throw new Error("process.exit called");
1016
- });
1017
- await expect(runNew({ marketplaceId: "pr-review" })).rejects.toThrow("process.exit called");
1018
- expect(exitSpy).toHaveBeenCalledWith(0);
1019
- // installMarketplaceWorkflow must NOT have been called
1020
- expect(mkt.installMarketplaceWorkflow).not.toHaveBeenCalled();
1021
- });
1022
- });
1023
- describe("runNew wizard — browse marketplace branch", () => {
1024
- afterEach(() => vi.restoreAllMocks());
1025
- it("fetches index and delegates to installMarketplaceWorkflow when user picks a marketplace entry", async () => {
1026
- const p = await import("@clack/prompts");
1027
- const mkt = await import("./marketplace.js");
1028
- p.select.mockResolvedValueOnce("__marketplace").mockResolvedValueOnce("pr-review");
1029
- mkt.fetchMarketplaceIndex.mockResolvedValueOnce([
1030
- { id: "pr-review", name: "PR Review", description: "Review PRs", skills: ["github"] },
1031
- ]);
1032
- mkt.installMarketplaceWorkflow.mockResolvedValueOnce({
1033
- installed: true,
1034
- adapted: false,
1035
- workflowPath: "/tmp/.sweny/workflows/pr-review.yml",
1036
- mismatches: [],
1037
- addedEnvKeys: 0,
1038
- });
1039
- await runNew();
1040
- expect(mkt.fetchMarketplaceIndex).toHaveBeenCalled();
1041
- expect(mkt.installMarketplaceWorkflow).toHaveBeenCalledWith("pr-review", expect.any(Object));
1042
- });
1043
- });