@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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,355 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveTemplateVars, buildE2eVars, buildSetupNode, buildReportNode, buildCleanupNode, buildFlowNodes, buildFlowWorkflow, buildE2eEnvTemplate, } from "./e2e.js";
3
+ import { workflowZ, validateWorkflow } from "../schema.js";
4
+ describe("resolveTemplateVars", () => {
5
+ it("replaces known variables", () => {
6
+ const result = resolveTemplateVars("Navigate to {base_url}/signup", {
7
+ base_url: "http://localhost:3000",
8
+ });
9
+ expect(result).toBe("Navigate to http://localhost:3000/signup");
10
+ });
11
+ it("leaves unknown variables untouched", () => {
12
+ const result = resolveTemplateVars("Hello {unknown}", {});
13
+ expect(result).toBe("Hello {unknown}");
14
+ });
15
+ it("replaces multiple occurrences of the same variable", () => {
16
+ const result = resolveTemplateVars("{base_url} and {base_url}", {
17
+ base_url: "http://localhost:3000",
18
+ });
19
+ expect(result).toBe("http://localhost:3000 and http://localhost:3000");
20
+ });
21
+ it("replaces multiple different variables", () => {
22
+ const result = resolveTemplateVars("email: {test_email}, pw: {test_password}", {
23
+ test_email: "e2e-123@app.test",
24
+ test_password: "E2eTest!123",
25
+ });
26
+ expect(result).toBe("email: e2e-123@app.test, pw: E2eTest!123");
27
+ });
28
+ it("handles empty string input", () => {
29
+ expect(resolveTemplateVars("", { foo: "bar" })).toBe("");
30
+ });
31
+ });
32
+ describe("buildE2eVars", () => {
33
+ it("auto-generates run_id when not in env", () => {
34
+ const vars = buildE2eVars({});
35
+ expect(vars.run_id).toMatch(/^\d+$/);
36
+ });
37
+ it("uses RUN_ID from env when provided", () => {
38
+ const vars = buildE2eVars({ RUN_ID: "custom-42" });
39
+ expect(vars.run_id).toBe("custom-42");
40
+ });
41
+ it("auto-generates test_email from run_id", () => {
42
+ const vars = buildE2eVars({ RUN_ID: "12345" });
43
+ expect(vars.test_email).toBe("e2e-12345@yourapp.test");
44
+ });
45
+ it("auto-generates test_password from run_id", () => {
46
+ const vars = buildE2eVars({ RUN_ID: "12345" });
47
+ expect(vars.test_password).toBe("E2eTest!12345");
48
+ });
49
+ it("reads base_url from E2E_BASE_URL", () => {
50
+ const vars = buildE2eVars({ E2E_BASE_URL: "https://myapp.com" });
51
+ expect(vars.base_url).toBe("https://myapp.com");
52
+ });
53
+ it("defaults base_url to http://localhost:3000", () => {
54
+ const vars = buildE2eVars({});
55
+ expect(vars.base_url).toBe("http://localhost:3000");
56
+ });
57
+ it("reads email/password from E2E_EMAIL and E2E_PASSWORD", () => {
58
+ const vars = buildE2eVars({
59
+ E2E_EMAIL: "test@example.com",
60
+ E2E_PASSWORD: "secret",
61
+ });
62
+ expect(vars.email).toBe("test@example.com");
63
+ expect(vars.password).toBe("secret");
64
+ });
65
+ it("falls back email/password to test_email/test_password when not in env", () => {
66
+ const vars = buildE2eVars({ RUN_ID: "99" });
67
+ expect(vars.email).toBe(vars.test_email);
68
+ expect(vars.password).toBe(vars.test_password);
69
+ });
70
+ it("picks up E2E_* custom vars", () => {
71
+ const vars = buildE2eVars({ E2E_API_KEY: "abc123" });
72
+ expect(vars.api_key).toBe("abc123");
73
+ });
74
+ });
75
+ describe("buildSetupNode", () => {
76
+ it("returns a node with agent-browser install instructions", () => {
77
+ const node = buildSetupNode();
78
+ expect(node.name).toBe("Browser Setup");
79
+ expect(node.instruction).toContain("agent-browser");
80
+ expect(node.instruction).toContain("npm install -g");
81
+ expect(node.skills).toEqual([]);
82
+ expect(node.output).toBeDefined();
83
+ });
84
+ it("includes daemon startup and readiness polling", () => {
85
+ const node = buildSetupNode();
86
+ expect(node.instruction).toContain("agent-browser &");
87
+ expect(node.instruction).toContain("agent-browser get url");
88
+ });
89
+ it("output schema has status enum [ready, fail]", () => {
90
+ const node = buildSetupNode();
91
+ expect(node.output).toEqual({
92
+ type: "object",
93
+ properties: {
94
+ status: { type: "string", enum: ["ready", "fail"] },
95
+ },
96
+ required: ["status"],
97
+ });
98
+ });
99
+ });
100
+ describe("buildReportNode", () => {
101
+ it("references the test node IDs in its instruction", () => {
102
+ const node = buildReportNode(["test_registration", "test_login"]);
103
+ expect(node.instruction).toContain("test_registration");
104
+ expect(node.instruction).toContain("test_login");
105
+ });
106
+ it("output schema has total, passed, failed, summary", () => {
107
+ const node = buildReportNode(["test_x"]);
108
+ expect(node.output).toEqual({
109
+ type: "object",
110
+ properties: {
111
+ total: { type: "number" },
112
+ passed: { type: "number" },
113
+ failed: { type: "number" },
114
+ summary: { type: "string" },
115
+ },
116
+ required: ["total", "passed", "failed", "summary"],
117
+ });
118
+ });
119
+ });
120
+ describe("buildCleanupNode", () => {
121
+ it("includes supabase-specific instructions for supabase backend", () => {
122
+ const node = buildCleanupNode("supabase");
123
+ expect(node.instruction).toContain("Supabase");
124
+ expect(node.instruction).toContain("e2e-");
125
+ expect(node.instruction).toContain("SUPABASE_SERVICE_ROLE_KEY");
126
+ });
127
+ it("includes firebase-specific instructions for firebase backend", () => {
128
+ const node = buildCleanupNode("firebase");
129
+ expect(node.instruction).toContain("Firebase");
130
+ });
131
+ it("includes generic instructions for other backends", () => {
132
+ const node = buildCleanupNode("other");
133
+ expect(node.instruction).toContain("e2e-{run_id}");
134
+ });
135
+ it("has no output schema", () => {
136
+ const node = buildCleanupNode("supabase");
137
+ expect(node.output).toBeUndefined();
138
+ });
139
+ });
140
+ describe("buildFlowNodes", () => {
141
+ describe("registration", () => {
142
+ it("returns a test_registration node", () => {
143
+ const { nodes, testNodeIds } = buildFlowNodes({
144
+ type: "registration",
145
+ path: "/signup",
146
+ fields: ["email", "password", "name"],
147
+ successRedirect: "/dashboard",
148
+ });
149
+ expect(nodes.test_registration).toBeDefined();
150
+ expect(nodes.test_registration.instruction).toContain("/signup");
151
+ expect(nodes.test_registration.instruction).toContain("{test_email}");
152
+ expect(nodes.test_registration.instruction).toContain("{test_password}");
153
+ expect(nodes.test_registration.instruction).toContain("agent-browser");
154
+ expect(testNodeIds).toEqual(["test_registration"]);
155
+ });
156
+ it("does not include a login node", () => {
157
+ const { nodes } = buildFlowNodes({ type: "registration", path: "/signup" });
158
+ expect(nodes.login).toBeUndefined();
159
+ });
160
+ it("includes field names in instruction", () => {
161
+ const { nodes } = buildFlowNodes({
162
+ type: "registration",
163
+ path: "/signup",
164
+ fields: ["email", "password", "name", "company"],
165
+ });
166
+ expect(nodes.test_registration.instruction).toContain("company");
167
+ });
168
+ });
169
+ describe("login", () => {
170
+ it("returns a test_login node", () => {
171
+ const { nodes, testNodeIds } = buildFlowNodes({ type: "login", path: "/login" });
172
+ expect(nodes.test_login).toBeDefined();
173
+ expect(nodes.test_login.instruction).toContain("/login");
174
+ expect(nodes.test_login.instruction).toContain("{email}");
175
+ expect(nodes.test_login.instruction).toContain("{password}");
176
+ expect(testNodeIds).toEqual(["test_login"]);
177
+ });
178
+ });
179
+ describe("purchase (auth-dependent)", () => {
180
+ it("returns both login and test_purchase nodes", () => {
181
+ const { nodes, testNodeIds } = buildFlowNodes({
182
+ type: "purchase",
183
+ path: "/pricing",
184
+ paymentProvider: "Stripe",
185
+ });
186
+ expect(nodes.login).toBeDefined();
187
+ expect(nodes.test_purchase).toBeDefined();
188
+ expect(nodes.login.instruction).toContain("{email}");
189
+ expect(nodes.test_purchase.instruction).toContain("/pricing");
190
+ expect(nodes.test_purchase.instruction).toContain("Stripe");
191
+ expect(testNodeIds).toEqual(["login", "test_purchase"]);
192
+ });
193
+ });
194
+ describe("custom", () => {
195
+ it("embeds the user description in the instruction", () => {
196
+ const { nodes } = buildFlowNodes({
197
+ type: "custom",
198
+ path: "/admin",
199
+ description: "Test the admin dashboard data export feature",
200
+ successCriteria: "CSV file downloads successfully",
201
+ });
202
+ expect(nodes.test_custom).toBeDefined();
203
+ expect(nodes.test_custom.instruction).toContain("Test the admin dashboard data export feature");
204
+ expect(nodes.test_custom.instruction).toContain("CSV file downloads successfully");
205
+ });
206
+ });
207
+ describe("all flow types return valid nodes", () => {
208
+ const flows = [
209
+ { type: "registration", path: "/signup" },
210
+ { type: "login", path: "/login" },
211
+ { type: "purchase", path: "/pricing" },
212
+ { type: "onboarding", path: "/onboarding" },
213
+ { type: "upgrade", path: "/upgrade" },
214
+ { type: "cancellation", path: "/cancel" },
215
+ { type: "custom", path: "/test", description: "custom test" },
216
+ ];
217
+ for (const flow of flows) {
218
+ it(`${flow.type}: every node has non-empty name, instruction, and skills array`, () => {
219
+ const { nodes } = buildFlowNodes(flow);
220
+ for (const [id, node] of Object.entries(nodes)) {
221
+ expect(node.name, `${id}.name`).toBeTruthy();
222
+ expect(node.instruction.length, `${id}.instruction`).toBeGreaterThan(0);
223
+ expect(Array.isArray(node.skills), `${id}.skills`).toBe(true);
224
+ }
225
+ });
226
+ }
227
+ });
228
+ describe("auth-dependent flows include login node", () => {
229
+ const authFlows = ["purchase", "onboarding", "upgrade", "cancellation"];
230
+ for (const type of authFlows) {
231
+ it(`${type} includes a login node`, () => {
232
+ const { nodes } = buildFlowNodes({ type, path: "/test" });
233
+ expect(nodes.login).toBeDefined();
234
+ expect(nodes.login.instruction).toContain("{email}");
235
+ });
236
+ }
237
+ });
238
+ });
239
+ describe("buildFlowWorkflow", () => {
240
+ it("returns a valid Workflow object for registration", () => {
241
+ const wf = buildFlowWorkflow({ type: "registration", path: "/signup" }, "http://localhost:3000");
242
+ expect(wf.id).toBe("e2e-registration");
243
+ expect(wf.entry).toBe("setup");
244
+ expect(wf.nodes.setup).toBeDefined();
245
+ expect(wf.nodes.test_registration).toBeDefined();
246
+ expect(wf.nodes.report).toBeDefined();
247
+ });
248
+ it("passes Zod schema validation", () => {
249
+ const wf = buildFlowWorkflow({ type: "registration", path: "/signup" }, "http://localhost:3000");
250
+ expect(() => workflowZ.parse(wf)).not.toThrow();
251
+ });
252
+ it("passes structural validation (no cycles, all reachable)", () => {
253
+ const wf = buildFlowWorkflow({ type: "registration", path: "/signup" }, "http://localhost:3000");
254
+ const errors = validateWorkflow(wf);
255
+ expect(errors).toEqual([]);
256
+ });
257
+ it("includes login node for auth-dependent flows", () => {
258
+ const wf = buildFlowWorkflow({ type: "purchase", path: "/pricing" }, "http://localhost:3000");
259
+ expect(wf.nodes.login).toBeDefined();
260
+ expect(wf.edges.some((e) => e.from === "setup" && e.to === "login")).toBe(true);
261
+ expect(wf.edges.some((e) => e.from === "login" && e.to === "test_purchase")).toBe(true);
262
+ });
263
+ it("adds cleanup node when enabled", () => {
264
+ const wf = buildFlowWorkflow({ type: "registration", path: "/signup" }, "http://localhost:3000", {
265
+ enabled: true,
266
+ backend: "supabase",
267
+ });
268
+ expect(wf.nodes.cleanup).toBeDefined();
269
+ expect(wf.edges.some((e) => e.to === "cleanup")).toBe(true);
270
+ expect(wf.edges.some((e) => e.from === "cleanup" && e.to === "report")).toBe(true);
271
+ });
272
+ it("skips cleanup node when not enabled", () => {
273
+ const wf = buildFlowWorkflow({ type: "registration", path: "/signup" }, "http://localhost:3000", {
274
+ enabled: false,
275
+ });
276
+ expect(wf.nodes.cleanup).toBeUndefined();
277
+ });
278
+ it("all 7 flow types produce valid workflows", () => {
279
+ const types = [
280
+ { type: "registration", path: "/signup" },
281
+ { type: "login", path: "/login" },
282
+ { type: "purchase", path: "/pricing" },
283
+ { type: "onboarding", path: "/onboarding" },
284
+ { type: "upgrade", path: "/upgrade" },
285
+ { type: "cancellation", path: "/cancel" },
286
+ { type: "custom", path: "/test" },
287
+ ];
288
+ for (const { type, path: flowPath } of types) {
289
+ const wf = buildFlowWorkflow({ type, path: flowPath, description: "test custom flow" }, "http://localhost:3000");
290
+ expect(() => workflowZ.parse(wf), `${type}: schema`).not.toThrow();
291
+ const errors = validateWorkflow(wf);
292
+ expect(errors, `${type}: structural`).toEqual([]);
293
+ }
294
+ });
295
+ it("includes failure edge from setup to report", () => {
296
+ const wf = buildFlowWorkflow({ type: "registration", path: "/signup" }, "http://localhost:3000");
297
+ expect(wf.edges.some((e) => e.from === "setup" && e.to === "report" && e.when?.includes("fail"))).toBe(true);
298
+ });
299
+ it("includes failure edges from login to report for auth flows", () => {
300
+ const wf = buildFlowWorkflow({ type: "purchase", path: "/pricing" }, "http://localhost:3000");
301
+ expect(wf.edges.some((e) => e.from === "login" && e.to === "report" && e.when?.includes("fail"))).toBe(true);
302
+ });
303
+ });
304
+ describe("buildE2eEnvTemplate", () => {
305
+ it("always includes E2E_BASE_URL", () => {
306
+ const env = buildE2eEnvTemplate({
307
+ flows: [{ type: "registration", path: "/signup" }],
308
+ baseUrl: "http://localhost:3000",
309
+ cleanup: { enabled: false },
310
+ });
311
+ expect(env).toContain("E2E_BASE_URL=http://localhost:3000");
312
+ });
313
+ it("includes E2E_EMAIL and E2E_PASSWORD for auth-dependent flows", () => {
314
+ const env = buildE2eEnvTemplate({
315
+ flows: [{ type: "purchase", path: "/pricing" }],
316
+ baseUrl: "http://localhost:3000",
317
+ cleanup: { enabled: false },
318
+ });
319
+ expect(env).toContain("E2E_EMAIL=");
320
+ expect(env).toContain("E2E_PASSWORD=");
321
+ });
322
+ it("does not include E2E_EMAIL for registration-only flows", () => {
323
+ const env = buildE2eEnvTemplate({
324
+ flows: [{ type: "registration", path: "/signup" }],
325
+ baseUrl: "http://localhost:3000",
326
+ cleanup: { enabled: false },
327
+ });
328
+ expect(env).not.toContain("E2E_EMAIL=");
329
+ });
330
+ it("includes SUPABASE vars when cleanup backend is supabase", () => {
331
+ const env = buildE2eEnvTemplate({
332
+ flows: [{ type: "registration", path: "/signup" }],
333
+ baseUrl: "http://localhost:3000",
334
+ cleanup: { enabled: true, backend: "supabase" },
335
+ });
336
+ expect(env).toContain("SUPABASE_URL=");
337
+ expect(env).toContain("SUPABASE_SERVICE_ROLE_KEY=");
338
+ });
339
+ it("includes FIREBASE var when cleanup backend is firebase", () => {
340
+ const env = buildE2eEnvTemplate({
341
+ flows: [{ type: "registration", path: "/signup" }],
342
+ baseUrl: "http://localhost:3000",
343
+ cleanup: { enabled: true, backend: "firebase" },
344
+ });
345
+ expect(env).toContain("FIREBASE_SERVICE_ACCOUNT_KEY=");
346
+ });
347
+ it("includes DATABASE_URL when cleanup backend is postgres", () => {
348
+ const env = buildE2eEnvTemplate({
349
+ flows: [{ type: "registration", path: "/signup" }],
350
+ baseUrl: "http://localhost:3000",
351
+ cleanup: { enabled: true, backend: "postgres" },
352
+ });
353
+ expect(env).toContain("DATABASE_URL=");
354
+ });
355
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * sweny init — Interactive setup wizard
3
+ *
4
+ * Pure functions for file generation + thin @clack/prompts interactive layer.
5
+ * Tests cover the pure functions; the interactive wizard is thin glue.
6
+ */
7
+ export interface Credential {
8
+ key: string;
9
+ hint?: string;
10
+ url?: string;
11
+ default?: string;
12
+ }
13
+ export interface InitSelections {
14
+ sourceControl: string;
15
+ observability: string | null;
16
+ issueTracker: string;
17
+ notification: string;
18
+ githubAction: boolean;
19
+ cronExpression: string | null;
20
+ }
21
+ export interface GitRemoteInfo {
22
+ provider: "github" | "gitlab";
23
+ remote: string;
24
+ }
25
+ export declare const PROVIDER_CREDENTIALS: Record<string, Credential[]>;
26
+ /**
27
+ * Gather all unique credentials required for the selected providers.
28
+ * Always includes ANTHROPIC_API_KEY. Deduplicates by key name.
29
+ */
30
+ export declare function collectCredentials(selections: InitSelections): Credential[];
31
+ /**
32
+ * Detect the git remote origin from `.git/config`.
33
+ * Returns provider + normalised remote (e.g. "github.com/owner/repo"), or null.
34
+ */
35
+ export declare function detectGitRemote(cwd: string): GitRemoteInfo | null;
36
+ /**
37
+ * Generate .sweny.yml content from selections.
38
+ */
39
+ export declare function buildSwenyYml(selections: InitSelections): string;
40
+ /**
41
+ * Generate .env template with comments for each credential.
42
+ */
43
+ export declare function buildEnvTemplate(credentials: Credential[]): string;
44
+ /**
45
+ * Generate a GitHub Actions workflow YAML for SWEny.
46
+ * Only includes credentials WITHOUT defaults as secrets.
47
+ */
48
+ export declare function buildActionWorkflow(credentials: Credential[], cronExpression: string): string;
49
+ /**
50
+ * Interactive setup wizard — prompts the user through provider selection,
51
+ * then generates `.sweny.yml`, `.env`, and an optional GitHub Action workflow.
52
+ */
53
+ export declare function runInit(): Promise<void>;