@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.
- package/README.md +44 -19
- package/bin/testkit.mjs +1 -1
- package/lib/cli/args.mjs +57 -0
- package/lib/cli/args.test.mjs +62 -0
- package/lib/cli/index.mjs +88 -0
- package/lib/config/index.mjs +294 -0
- package/lib/config/index.test.mjs +12 -0
- package/lib/config/model.mjs +422 -0
- package/lib/config/model.test.mjs +193 -0
- package/lib/database/fingerprint.mjs +61 -0
- package/lib/database/fingerprint.test.mjs +93 -0
- package/lib/{database.mjs → database/index.mjs} +45 -160
- package/lib/database/naming.mjs +47 -0
- package/lib/database/naming.test.mjs +39 -0
- package/lib/database/state.mjs +52 -0
- package/lib/database/state.test.mjs +66 -0
- package/lib/reporters/playwright.mjs +125 -0
- package/lib/reporters/playwright.test.mjs +73 -0
- package/lib/runner/index.mjs +1221 -0
- package/lib/runner/metadata.mjs +55 -0
- package/lib/runner/metadata.test.mjs +52 -0
- package/lib/runner/planning.mjs +270 -0
- package/lib/runner/planning.test.mjs +127 -0
- package/lib/runner/results.mjs +285 -0
- package/lib/runner/results.test.mjs +144 -0
- package/lib/runner/state.mjs +71 -0
- package/lib/runner/state.test.mjs +64 -0
- package/lib/runner/template.mjs +320 -0
- package/lib/runner/template.test.mjs +150 -0
- package/lib/telemetry/index.mjs +43 -0
- package/lib/timing/index.mjs +73 -0
- package/lib/timing/index.test.mjs +64 -0
- package/package.json +11 -3
- package/infra/neon-down.sh +0 -18
- package/infra/neon-up.sh +0 -124
- package/lib/cli.mjs +0 -132
- package/lib/config.mjs +0 -666
- package/lib/exec.mjs +0 -20
- 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
|
-
}
|