@elench/testkit 0.1.16 → 0.1.18

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 (39) hide show
  1. package/README.md +44 -19
  2. package/bin/testkit.mjs +1 -1
  3. package/lib/cli/args.mjs +57 -0
  4. package/lib/cli/args.test.mjs +62 -0
  5. package/lib/cli/index.mjs +88 -0
  6. package/lib/config/index.mjs +294 -0
  7. package/lib/config/index.test.mjs +12 -0
  8. package/lib/config/model.mjs +422 -0
  9. package/lib/config/model.test.mjs +193 -0
  10. package/lib/database/fingerprint.mjs +61 -0
  11. package/lib/database/fingerprint.test.mjs +93 -0
  12. package/lib/{database.mjs → database/index.mjs} +45 -160
  13. package/lib/database/naming.mjs +47 -0
  14. package/lib/database/naming.test.mjs +39 -0
  15. package/lib/database/state.mjs +52 -0
  16. package/lib/database/state.test.mjs +66 -0
  17. package/lib/reporters/playwright.mjs +125 -0
  18. package/lib/reporters/playwright.test.mjs +73 -0
  19. package/lib/runner/index.mjs +1221 -0
  20. package/lib/runner/metadata.mjs +55 -0
  21. package/lib/runner/metadata.test.mjs +52 -0
  22. package/lib/runner/planning.mjs +270 -0
  23. package/lib/runner/planning.test.mjs +127 -0
  24. package/lib/runner/results.mjs +285 -0
  25. package/lib/runner/results.test.mjs +144 -0
  26. package/lib/runner/state.mjs +71 -0
  27. package/lib/runner/state.test.mjs +64 -0
  28. package/lib/runner/template.mjs +320 -0
  29. package/lib/runner/template.test.mjs +150 -0
  30. package/lib/telemetry/index.mjs +43 -0
  31. package/lib/timing/index.mjs +73 -0
  32. package/lib/timing/index.test.mjs +64 -0
  33. package/package.json +11 -3
  34. package/infra/neon-down.sh +0 -18
  35. package/infra/neon-up.sh +0 -124
  36. package/lib/cli.mjs +0 -132
  37. package/lib/config.mjs +0 -666
  38. package/lib/exec.mjs +0 -20
  39. package/lib/runner.mjs +0 -1165
package/infra/neon-up.sh DELETED
@@ -1,124 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Ensures a persistent Neon test branch exists and resets its data.
3
- # Creates the branch on first run; truncates tables on subsequent runs.
4
- # Requires: NEON_API_KEY, NEON_PROJECT_ID
5
- set -eo pipefail
6
-
7
- STATE_DIR="${STATE_DIR:-.state}"
8
- NEON_DB_NAME="${NEON_DB_NAME:-neondb}"
9
- BRANCH_NAME="${NEON_BRANCH_NAME:-testkit}"
10
- NEON_API="https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID"
11
-
12
- mkdir -p "$STATE_DIR"
13
-
14
- # ── 1. Check state file ──────────────────────────────────────────────────
15
- BRANCH_ID=""
16
- if [ -f "$STATE_DIR/neon_branch_id" ]; then
17
- STORED_ID=$(cat "$STATE_DIR/neon_branch_id")
18
- if curl -sf "$NEON_API/branches/$STORED_ID" \
19
- -H "Authorization: Bearer $NEON_API_KEY" >/dev/null 2>&1; then
20
- BRANCH_ID="$STORED_ID"
21
- echo "Neon branch exists: $BRANCH_ID"
22
- else
23
- echo "Stored branch $STORED_ID gone — will discover or create"
24
- rm -f "$STATE_DIR/neon_branch_id"
25
- fi
26
- fi
27
-
28
- # ── 2. Discover existing branch by name ──────────────────────────────────
29
- if [ -z "$BRANCH_ID" ]; then
30
- EXISTING_ID=$(curl -sf "$NEON_API/branches" \
31
- -H "Authorization: Bearer $NEON_API_KEY" \
32
- | jq -r --arg name "$BRANCH_NAME" '.branches[] | select(.name == $name) | .id' | head -1)
33
-
34
- if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
35
- BRANCH_ID="$EXISTING_ID"
36
- echo "$BRANCH_ID" > "$STATE_DIR/neon_branch_id"
37
- echo "Discovered existing Neon branch '$BRANCH_NAME': $BRANCH_ID"
38
- fi
39
- fi
40
-
41
- # ── 3. Create branch if needed ───────────────────────────────────────────
42
- if [ -z "$BRANCH_ID" ]; then
43
- echo "Creating Neon branch: $BRANCH_NAME"
44
- RESPONSE=$(curl -sf -X POST "$NEON_API/branches" \
45
- -H "Authorization: Bearer $NEON_API_KEY" \
46
- -H "Content-Type: application/json" \
47
- -d "{
48
- \"branch\": { \"name\": \"$BRANCH_NAME\" },
49
- \"endpoints\": [{ \"type\": \"read_write\", \"suspend_timeout_seconds\": 300 }]
50
- }")
51
-
52
- BRANCH_ID=$(echo "$RESPONSE" | jq -r '.branch.id')
53
- if [ -z "$BRANCH_ID" ] || [ "$BRANCH_ID" = "null" ]; then
54
- # Create failed — may be a 409 race (another parallel service created it).
55
- # Re-discover by name before giving up.
56
- BRANCH_ID=$(curl -sf "$NEON_API/branches" \
57
- -H "Authorization: Bearer $NEON_API_KEY" \
58
- | jq -r --arg name "$BRANCH_NAME" '.branches[] | select(.name == $name) | .id' | head -1)
59
-
60
- if [ -z "$BRANCH_ID" ] || [ "$BRANCH_ID" = "null" ]; then
61
- echo "ERROR: Failed to create or discover branch '$BRANCH_NAME'"
62
- exit 1
63
- fi
64
- echo "Branch created by another process — discovered: $BRANCH_ID"
65
- fi
66
- echo "$BRANCH_ID" > "$STATE_DIR/neon_branch_id"
67
-
68
- ENDPOINT_ID=$(echo "$RESPONSE" | jq -r '.endpoints[0].id')
69
- if [ -n "$ENDPOINT_ID" ] && [ "$ENDPOINT_ID" != "null" ]; then
70
- echo "Waiting for endpoint $ENDPOINT_ID to become active..."
71
- for i in $(seq 1 30); do
72
- EP_STATE=$(curl -sf "$NEON_API/endpoints/$ENDPOINT_ID" \
73
- -H "Authorization: Bearer $NEON_API_KEY" \
74
- | jq -r '.endpoint.current_state')
75
- if [ "$EP_STATE" = "active" ] || [ "$EP_STATE" = "idle" ]; then
76
- echo "Endpoint ready (state: $EP_STATE)"
77
- break
78
- fi
79
- if [ "$i" -eq 30 ]; then
80
- echo "WARNING: Endpoint still '$EP_STATE' after 30s"
81
- fi
82
- sleep 1
83
- done
84
- fi
85
- fi
86
-
87
- # ── Get connection URI ───────────────────────────────────────────────────
88
- CONN_URI=$(curl -sf "$NEON_API/connection_uri?branch_id=$BRANCH_ID&database_name=$NEON_DB_NAME&role_name=neondb_owner" \
89
- -H "Authorization: Bearer $NEON_API_KEY" \
90
- | jq -r '.uri')
91
-
92
- if [ -z "$CONN_URI" ] || [ "$CONN_URI" = "null" ]; then
93
- echo "ERROR: Failed to get connection URI"
94
- exit 1
95
- fi
96
- echo "$CONN_URI" > "$STATE_DIR/database_url"
97
-
98
- # ── Reset test data ─────────────────────────────────────────────────────
99
- NEON_RESET="${NEON_RESET:-true}"
100
- if [ "$NEON_RESET" = "false" ]; then
101
- echo "Reset disabled — keeping fork data"
102
- elif command -v psql &>/dev/null; then
103
- echo "Resetting test data..."
104
- psql "$CONN_URI" -q -c "
105
- DO \$\$
106
- DECLARE r RECORD;
107
- BEGIN
108
- FOR r IN (
109
- SELECT tablename FROM pg_tables
110
- WHERE schemaname = 'public'
111
- AND tablename NOT LIKE '%migration%'
112
- AND tablename NOT LIKE 'goose_%'
113
- AND tablename NOT LIKE 'drizzle_%'
114
- ) LOOP
115
- EXECUTE 'TRUNCATE TABLE public.' || quote_ident(r.tablename) || ' CASCADE';
116
- END LOOP;
117
- END \$\$;
118
- " 2>/dev/null && echo "Tables truncated" || echo "First run — no tables to reset"
119
- else
120
- echo "WARNING: psql not available — skipping data reset"
121
- fi
122
-
123
- echo "Neon branch ready: $BRANCH_ID"
124
- echo "Database URL saved to $STATE_DIR/database_url"
package/lib/cli.mjs DELETED
@@ -1,132 +0,0 @@
1
- import { cac } from "cac";
2
- import { loadConfigs, getServiceNames, isSiblingProduct } from "./config.mjs";
3
- import * as runner from "./runner.mjs";
4
-
5
- const SUITE_TYPES = new Set(["int", "integration", "e2e", "dal", "load", "all"]);
6
- const LIFECYCLE = new Set(["status", "destroy"]);
7
- const RESERVED = new Set([...SUITE_TYPES, ...LIFECYCLE]);
8
-
9
- export function run() {
10
- const cli = cac("testkit");
11
-
12
- cli
13
- .command("[first] [second] [third]", "Run test suites (int, e2e, dal, all)")
14
- .option("-s, --suite <name>", "Run specific suite(s)", { default: [] })
15
- .option("--dir <path>", "Explicit product directory")
16
- .option("--jobs <n>", "Number of isolated worker stacks per service", {
17
- default: "1",
18
- })
19
- .option("--db-backend <name>", "Database backend override (neon, local)")
20
- .option("--shard <i/n>", "Run only shard i of n at suite granularity")
21
- .option("--framework <name>", "Filter by framework (k6, playwright, all)", {
22
- default: "all",
23
- })
24
- .action(async (first, second, third, options) => {
25
- // Resolve: service filter, suite type, and --dir.
26
- //
27
- // From product dir:
28
- // testkit → all services, all types
29
- // testkit int -s health → all services, int, health
30
- // testkit avocado_api int → one service, int
31
- // testkit avocado_api → one service, all types
32
- //
33
- // From workspace root:
34
- // testkit --dir outreach int → all services, int
35
- // testkit --dir avocado avocado_api int → one service, int
36
- //
37
- // Legacy (sibling dir as first arg):
38
- // testkit outreach int → --dir outreach, int
39
-
40
- let service = null;
41
- let type = null;
42
-
43
- // If first arg is a sibling dir with a manifest, treat it as --dir
44
- if (first && !RESERVED.has(first) && !options.dir && isSiblingProduct(first)) {
45
- options.dir = first;
46
- first = second;
47
- second = third;
48
- third = undefined;
49
- }
50
-
51
- // Now resolve service vs type from remaining args
52
- const serviceNames = new Set(getServiceNames(options.dir));
53
-
54
- if (first && serviceNames.has(first)) {
55
- service = first;
56
- type = second || null;
57
- } else if (first && RESERVED.has(first)) {
58
- type = first;
59
- } else if (first) {
60
- // Unknown arg — might be a service name that doesn't exist
61
- throw new Error(
62
- `Unknown argument "${first}". Expected a service name (${[...serviceNames].join(", ") || "none found"}) ` +
63
- `or suite type (int, e2e, dal, all).`
64
- );
65
- }
66
-
67
- const allConfigs = loadConfigs({ dir: options.dir, dbBackend: options.dbBackend });
68
- const configs = service
69
- ? allConfigs.filter((config) => config.name === service)
70
- : allConfigs;
71
- if (service && configs.length === 0) {
72
- const available = allConfigs.map((config) => config.name).join(", ");
73
- throw new Error(`Service "${service}" not found. Available: ${available}`);
74
- }
75
-
76
- // Lifecycle commands
77
- if (type === "status" || type === "destroy") {
78
- for (const config of configs) {
79
- if (configs.length > 1) console.log(`\n── ${config.name} ──`);
80
- if (type === "status") runner.showStatus(config);
81
- else await runner.destroy(config);
82
- }
83
- return;
84
- }
85
-
86
- if (!["all", "k6", "playwright"].includes(options.framework)) {
87
- throw new Error(
88
- `Unknown framework "${options.framework}". Expected one of: all, k6, playwright.`
89
- );
90
- }
91
-
92
- const jobs = Number.parseInt(String(options.jobs), 10);
93
- if (!Number.isInteger(jobs) || jobs <= 0) {
94
- throw new Error(`Invalid --jobs value "${options.jobs}". Expected a positive integer.`);
95
- }
96
-
97
- let shard = null;
98
- if (options.shard) {
99
- const match = String(options.shard).match(/^(\d+)\/(\d+)$/);
100
- if (!match) {
101
- throw new Error(
102
- `Invalid --shard value "${options.shard}". Expected the form "i/n", e.g. 1/3.`
103
- );
104
- }
105
- const index = Number.parseInt(match[1], 10);
106
- const total = Number.parseInt(match[2], 10);
107
- if (index <= 0 || total <= 0 || index > total) {
108
- throw new Error(
109
- `Invalid --shard value "${options.shard}". Expected 1 <= i <= n.`
110
- );
111
- }
112
- shard = { index, total };
113
- }
114
-
115
- const suiteType = type || "all";
116
- const suiteNames = Array.isArray(options.suite) ? options.suite : [options.suite].filter(Boolean);
117
- await runner.runAll(
118
- configs,
119
- suiteType,
120
- suiteNames,
121
- {
122
- ...options,
123
- jobs,
124
- shard,
125
- },
126
- allConfigs
127
- );
128
- });
129
-
130
- cli.help();
131
- cli.parse();
132
- }