@sweny-ai/core 0.1.32 → 0.1.34
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.test.js +283 -0
- package/package.json +2 -2
package/dist/cli/e2e.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { stringify as stringifyYaml, parse as parseYaml } from "yaml";
|
|
2
3
|
import { resolveTemplateVars, buildE2eVars, buildSetupNode, buildReportNode, buildCleanupNode, buildFlowNodes, buildFlowWorkflow, buildE2eEnvTemplate, } from "./e2e.js";
|
|
3
4
|
import { workflowZ, validateWorkflow } from "../schema.js";
|
|
4
5
|
describe("resolveTemplateVars", () => {
|
|
@@ -352,4 +353,286 @@ describe("buildE2eEnvTemplate", () => {
|
|
|
352
353
|
});
|
|
353
354
|
expect(env).toContain("DATABASE_URL=");
|
|
354
355
|
});
|
|
356
|
+
it("includes auth vars for login-only flows", () => {
|
|
357
|
+
const env = buildE2eEnvTemplate({
|
|
358
|
+
flows: [{ type: "login", path: "/login" }],
|
|
359
|
+
baseUrl: "http://localhost:3000",
|
|
360
|
+
cleanup: { enabled: false },
|
|
361
|
+
});
|
|
362
|
+
expect(env).toContain("E2E_EMAIL=");
|
|
363
|
+
expect(env).toContain("E2E_PASSWORD=");
|
|
364
|
+
});
|
|
365
|
+
it("includes auth vars for mixed flows when any need auth", () => {
|
|
366
|
+
const env = buildE2eEnvTemplate({
|
|
367
|
+
flows: [
|
|
368
|
+
{ type: "registration", path: "/signup" },
|
|
369
|
+
{ type: "purchase", path: "/pricing" },
|
|
370
|
+
],
|
|
371
|
+
baseUrl: "http://localhost:3000",
|
|
372
|
+
cleanup: { enabled: false },
|
|
373
|
+
});
|
|
374
|
+
expect(env).toContain("E2E_EMAIL=");
|
|
375
|
+
expect(env).toContain("E2E_PASSWORD=");
|
|
376
|
+
});
|
|
377
|
+
it("does not include cleanup vars when cleanup is disabled", () => {
|
|
378
|
+
const env = buildE2eEnvTemplate({
|
|
379
|
+
flows: [{ type: "registration", path: "/signup" }],
|
|
380
|
+
baseUrl: "http://localhost:3000",
|
|
381
|
+
cleanup: { enabled: false },
|
|
382
|
+
});
|
|
383
|
+
expect(env).not.toContain("SUPABASE_URL");
|
|
384
|
+
expect(env).not.toContain("FIREBASE_SERVICE_ACCOUNT_KEY");
|
|
385
|
+
expect(env).not.toContain("DATABASE_URL");
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
// ── Additional coverage: edge cases and robustness ────────────────────
|
|
389
|
+
describe("buildE2eVars — edge cases", () => {
|
|
390
|
+
it("does not duplicate base_url from E2E_BASE_URL custom var pickup", () => {
|
|
391
|
+
const vars = buildE2eVars({ E2E_BASE_URL: "https://myapp.com" });
|
|
392
|
+
// base_url should exist exactly once with the correct value
|
|
393
|
+
expect(vars.base_url).toBe("https://myapp.com");
|
|
394
|
+
// The E2E_BASE_URL should NOT appear as a separate key
|
|
395
|
+
expect(vars).not.toHaveProperty("base_url_extra");
|
|
396
|
+
});
|
|
397
|
+
it("excludes E2E_EMAIL and E2E_PASSWORD from custom var pickup", () => {
|
|
398
|
+
const vars = buildE2eVars({
|
|
399
|
+
E2E_EMAIL: "test@example.com",
|
|
400
|
+
E2E_PASSWORD: "secret",
|
|
401
|
+
E2E_API_KEY: "abc",
|
|
402
|
+
});
|
|
403
|
+
// email/password should come from the explicit handling, not custom pickup
|
|
404
|
+
expect(vars.email).toBe("test@example.com");
|
|
405
|
+
expect(vars.password).toBe("secret");
|
|
406
|
+
expect(vars.api_key).toBe("abc");
|
|
407
|
+
// Should NOT have duplicates from the E2E_* stripping logic
|
|
408
|
+
expect(Object.keys(vars).filter((k) => k === "email")).toHaveLength(1);
|
|
409
|
+
expect(Object.keys(vars).filter((k) => k === "password")).toHaveLength(1);
|
|
410
|
+
});
|
|
411
|
+
it("ignores E2E_* vars with empty string values", () => {
|
|
412
|
+
const vars = buildE2eVars({ E2E_EMPTY: "" });
|
|
413
|
+
expect(vars).not.toHaveProperty("empty");
|
|
414
|
+
});
|
|
415
|
+
it("ignores E2E_* vars with undefined values", () => {
|
|
416
|
+
const vars = buildE2eVars({ E2E_UNDEF: undefined });
|
|
417
|
+
expect(vars).not.toHaveProperty("undef");
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
describe("buildFlowNodes — login node content", () => {
|
|
421
|
+
it("login node for auth-dependent flows contains agent-browser commands", () => {
|
|
422
|
+
const { nodes } = buildFlowNodes({ type: "purchase", path: "/pricing" });
|
|
423
|
+
const instruction = nodes.login.instruction;
|
|
424
|
+
expect(instruction).toContain("agent-browser open");
|
|
425
|
+
expect(instruction).toContain("agent-browser snapshot");
|
|
426
|
+
expect(instruction).toContain("agent-browser fill");
|
|
427
|
+
expect(instruction).toContain("agent-browser click");
|
|
428
|
+
});
|
|
429
|
+
it("login node uses {email} and {password} (not test_email/test_password)", () => {
|
|
430
|
+
const { nodes } = buildFlowNodes({ type: "onboarding", path: "/onboarding" });
|
|
431
|
+
expect(nodes.login.instruction).toContain("{email}");
|
|
432
|
+
expect(nodes.login.instruction).toContain("{password}");
|
|
433
|
+
});
|
|
434
|
+
it("login node has pass/fail output schema", () => {
|
|
435
|
+
const { nodes } = buildFlowNodes({ type: "upgrade", path: "/upgrade" });
|
|
436
|
+
expect(nodes.login.output).toEqual({
|
|
437
|
+
type: "object",
|
|
438
|
+
properties: {
|
|
439
|
+
status: { type: "string", enum: ["pass", "fail"] },
|
|
440
|
+
error: { type: "string" },
|
|
441
|
+
},
|
|
442
|
+
required: ["status"],
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
describe("buildFlowNodes — custom flow defaults", () => {
|
|
447
|
+
it("uses default description when none provided", () => {
|
|
448
|
+
const { nodes } = buildFlowNodes({ type: "custom", path: "/test" });
|
|
449
|
+
expect(nodes.test_custom.instruction).toContain("Perform the custom test flow");
|
|
450
|
+
});
|
|
451
|
+
it("uses default success criteria when none provided", () => {
|
|
452
|
+
const { nodes } = buildFlowNodes({ type: "custom", path: "/test" });
|
|
453
|
+
expect(nodes.test_custom.instruction).toContain("expected outcome is achieved without errors");
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
describe("buildFlowNodes — all test nodes have correct output schema", () => {
|
|
457
|
+
const flows = [
|
|
458
|
+
{ type: "registration", path: "/signup" },
|
|
459
|
+
{ type: "login", path: "/login" },
|
|
460
|
+
{ type: "purchase", path: "/pricing" },
|
|
461
|
+
{ type: "onboarding", path: "/onboarding" },
|
|
462
|
+
{ type: "upgrade", path: "/upgrade" },
|
|
463
|
+
{ type: "cancellation", path: "/cancel" },
|
|
464
|
+
{ type: "custom", path: "/test", description: "test" },
|
|
465
|
+
];
|
|
466
|
+
for (const flow of flows) {
|
|
467
|
+
it(`${flow.type}: test node has pass/fail output schema`, () => {
|
|
468
|
+
const { nodes, testNodeIds } = buildFlowNodes(flow);
|
|
469
|
+
const testNodeId = testNodeIds[testNodeIds.length - 1]; // last one is the actual test (not login)
|
|
470
|
+
const node = nodes[testNodeId];
|
|
471
|
+
expect(node.output).toEqual({
|
|
472
|
+
type: "object",
|
|
473
|
+
properties: {
|
|
474
|
+
status: { type: "string", enum: ["pass", "fail"] },
|
|
475
|
+
error: { type: "string" },
|
|
476
|
+
},
|
|
477
|
+
required: ["status"],
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
describe("buildFlowWorkflow — edge chain topology", () => {
|
|
483
|
+
it("registration: setup → test → report (3 edges)", () => {
|
|
484
|
+
const wf = buildFlowWorkflow({ type: "registration", path: "/signup" }, "http://localhost:3000");
|
|
485
|
+
expect(wf.edges).toEqual([
|
|
486
|
+
{ from: "setup", to: "test_registration", when: "setup status is ready" },
|
|
487
|
+
{ from: "setup", to: "report", when: "setup status is fail" },
|
|
488
|
+
{ from: "test_registration", to: "report" },
|
|
489
|
+
]);
|
|
490
|
+
});
|
|
491
|
+
it("purchase: setup → login → test → report with failure edges", () => {
|
|
492
|
+
const wf = buildFlowWorkflow({ type: "purchase", path: "/pricing" }, "http://localhost:3000");
|
|
493
|
+
expect(wf.edges).toEqual([
|
|
494
|
+
{ from: "setup", to: "login", when: "setup status is ready" },
|
|
495
|
+
{ from: "setup", to: "report", when: "setup status is fail" },
|
|
496
|
+
{ from: "login", to: "test_purchase", when: "login status is pass" },
|
|
497
|
+
{ from: "login", to: "report", when: "login status is fail" },
|
|
498
|
+
{ from: "test_purchase", to: "report" },
|
|
499
|
+
]);
|
|
500
|
+
});
|
|
501
|
+
it("registration with cleanup: setup → test → cleanup → report", () => {
|
|
502
|
+
const wf = buildFlowWorkflow({ type: "registration", path: "/signup" }, "http://localhost:3000", {
|
|
503
|
+
enabled: true,
|
|
504
|
+
backend: "supabase",
|
|
505
|
+
});
|
|
506
|
+
expect(wf.edges).toEqual([
|
|
507
|
+
{ from: "setup", to: "test_registration", when: "setup status is ready" },
|
|
508
|
+
{ from: "setup", to: "report", when: "setup status is fail" },
|
|
509
|
+
{ from: "test_registration", to: "cleanup" },
|
|
510
|
+
{ from: "cleanup", to: "report" },
|
|
511
|
+
]);
|
|
512
|
+
});
|
|
513
|
+
it("purchase with cleanup: setup → login → test → cleanup → report", () => {
|
|
514
|
+
const wf = buildFlowWorkflow({ type: "purchase", path: "/pricing" }, "http://localhost:3000", {
|
|
515
|
+
enabled: true,
|
|
516
|
+
backend: "postgres",
|
|
517
|
+
});
|
|
518
|
+
expect(wf.edges).toEqual([
|
|
519
|
+
{ from: "setup", to: "login", when: "setup status is ready" },
|
|
520
|
+
{ from: "setup", to: "report", when: "setup status is fail" },
|
|
521
|
+
{ from: "login", to: "test_purchase", when: "login status is pass" },
|
|
522
|
+
{ from: "login", to: "report", when: "login status is fail" },
|
|
523
|
+
{ from: "test_purchase", to: "cleanup" },
|
|
524
|
+
{ from: "cleanup", to: "report" },
|
|
525
|
+
]);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
describe("YAML round-trip — generated workflows survive stringify→parse→validate", () => {
|
|
529
|
+
const flowConfigs = [
|
|
530
|
+
{ type: "registration", path: "/signup", fields: ["email", "password", "name"] },
|
|
531
|
+
{ type: "login", path: "/login" },
|
|
532
|
+
{ type: "purchase", path: "/pricing", paymentProvider: "Stripe" },
|
|
533
|
+
{ type: "onboarding", path: "/onboarding" },
|
|
534
|
+
{ type: "upgrade", path: "/upgrade" },
|
|
535
|
+
{ type: "cancellation", path: "/cancel" },
|
|
536
|
+
{ type: "custom", path: "/test", description: "Export user data as CSV" },
|
|
537
|
+
];
|
|
538
|
+
for (const flow of flowConfigs) {
|
|
539
|
+
it(`${flow.type}: stringifyYaml → parseYaml → workflowZ.parse succeeds`, () => {
|
|
540
|
+
const wf = buildFlowWorkflow(flow, "http://localhost:3000");
|
|
541
|
+
const yaml = stringifyYaml(wf, { indent: 2, lineWidth: 120 });
|
|
542
|
+
const parsed = parseYaml(yaml);
|
|
543
|
+
// Zod schema validation
|
|
544
|
+
expect(() => workflowZ.parse(parsed)).not.toThrow();
|
|
545
|
+
// Structural validation (cycles, reachability)
|
|
546
|
+
const errors = validateWorkflow(parsed);
|
|
547
|
+
expect(errors).toEqual([]);
|
|
548
|
+
// Key fields survive round-trip
|
|
549
|
+
expect(parsed.id).toBe(wf.id);
|
|
550
|
+
expect(parsed.entry).toBe("setup");
|
|
551
|
+
expect(Object.keys(parsed.nodes)).toEqual(Object.keys(wf.nodes));
|
|
552
|
+
expect(parsed.edges).toHaveLength(wf.edges.length);
|
|
553
|
+
});
|
|
554
|
+
it(`${flow.type} with cleanup: round-trip with cleanup node`, () => {
|
|
555
|
+
const wf = buildFlowWorkflow(flow, "https://staging.example.com", {
|
|
556
|
+
enabled: true,
|
|
557
|
+
backend: "supabase",
|
|
558
|
+
});
|
|
559
|
+
const yaml = stringifyYaml(wf, { indent: 2, lineWidth: 120 });
|
|
560
|
+
const parsed = parseYaml(yaml);
|
|
561
|
+
expect(() => workflowZ.parse(parsed)).not.toThrow();
|
|
562
|
+
expect(validateWorkflow(parsed)).toEqual([]);
|
|
563
|
+
expect(parsed.nodes.cleanup).toBeDefined();
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
describe("template variable completeness — all {vars} in instructions are resolvable", () => {
|
|
568
|
+
const ALL_FLOW_TYPES = [
|
|
569
|
+
{ type: "registration", path: "/signup", fields: ["email", "password"] },
|
|
570
|
+
{ type: "login", path: "/login" },
|
|
571
|
+
{ type: "purchase", path: "/pricing", paymentProvider: "Stripe" },
|
|
572
|
+
{ type: "onboarding", path: "/onboarding" },
|
|
573
|
+
{ type: "upgrade", path: "/upgrade" },
|
|
574
|
+
{ type: "cancellation", path: "/cancel" },
|
|
575
|
+
{ type: "custom", path: "/test", description: "test flow" },
|
|
576
|
+
];
|
|
577
|
+
// Build vars with a known run_id so we can predict all values
|
|
578
|
+
const vars = buildE2eVars({ RUN_ID: "12345", E2E_BASE_URL: "http://localhost:3000" });
|
|
579
|
+
for (const flow of ALL_FLOW_TYPES) {
|
|
580
|
+
it(`${flow.type}: all template vars in instructions resolve to values`, () => {
|
|
581
|
+
const wf = buildFlowWorkflow(flow, "http://localhost:3000");
|
|
582
|
+
for (const [nodeId, node] of Object.entries(wf.nodes)) {
|
|
583
|
+
const resolved = resolveTemplateVars(node.instruction, vars);
|
|
584
|
+
// After resolution, no {word} placeholders should remain
|
|
585
|
+
// (except for agent-browser refs like @<ref> which are not {vars})
|
|
586
|
+
const unresolvedVars = resolved.match(/\{(\w+)\}/g) || [];
|
|
587
|
+
// Filter out intentional placeholders that are instructions, not template vars
|
|
588
|
+
const actualUnresolved = unresolvedVars.filter((v) => !["SUPABASE_URL", "SUPABASE_SERVICE_ROLE_KEY"].some((env) => v.includes(env)));
|
|
589
|
+
expect(actualUnresolved, `${nodeId} has unresolved vars: ${actualUnresolved.join(", ")}`).toEqual([]);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
describe("buildSetupNode — instruction quality", () => {
|
|
595
|
+
it("lists all essential agent-browser commands", () => {
|
|
596
|
+
const node = buildSetupNode();
|
|
597
|
+
const essentialCommands = [
|
|
598
|
+
"agent-browser open",
|
|
599
|
+
"agent-browser snapshot",
|
|
600
|
+
"agent-browser click",
|
|
601
|
+
"agent-browser fill",
|
|
602
|
+
"agent-browser press",
|
|
603
|
+
"agent-browser get url",
|
|
604
|
+
"agent-browser screenshot",
|
|
605
|
+
];
|
|
606
|
+
for (const cmd of essentialCommands) {
|
|
607
|
+
expect(node.instruction, `missing: ${cmd}`).toContain(cmd);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
it("includes timeout/retry instructions", () => {
|
|
611
|
+
const node = buildSetupNode();
|
|
612
|
+
expect(node.instruction).toContain("30 seconds");
|
|
613
|
+
expect(node.instruction).toContain("2 seconds");
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
describe("buildFlowWorkflow — node naming conventions", () => {
|
|
617
|
+
it("workflow id follows e2e-{type} pattern", () => {
|
|
618
|
+
const types = ["registration", "login", "purchase", "onboarding", "upgrade", "cancellation", "custom"];
|
|
619
|
+
for (const type of types) {
|
|
620
|
+
const wf = buildFlowWorkflow({ type, path: "/test", description: "test" }, "http://localhost:3000");
|
|
621
|
+
expect(wf.id).toBe(`e2e-${type}`);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
it("workflow name follows E2E: {Type} pattern", () => {
|
|
625
|
+
const wf = buildFlowWorkflow({ type: "registration", path: "/signup" }, "http://localhost:3000");
|
|
626
|
+
expect(wf.name).toBe("E2E: Registration");
|
|
627
|
+
});
|
|
628
|
+
it("every node has a non-empty name", () => {
|
|
629
|
+
const wf = buildFlowWorkflow({ type: "purchase", path: "/pricing" }, "http://localhost:3000", {
|
|
630
|
+
enabled: true,
|
|
631
|
+
backend: "supabase",
|
|
632
|
+
});
|
|
633
|
+
for (const [id, node] of Object.entries(wf.nodes)) {
|
|
634
|
+
expect(node.name, `${id} has empty name`).toBeTruthy();
|
|
635
|
+
expect(node.name.length, `${id} name too short`).toBeGreaterThan(2);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
355
638
|
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sweny-ai/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sweny": "./dist/cli/main.js"
|
|
7
7
|
},
|
|
8
|
-
"description": "Skill library
|
|
8
|
+
"description": "Skill library, DAG workflow orchestration, and E2E browser testing powered by Claude",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./dist/index.js",
|