@activate-core/test-forge 0.8.1 → 0.9.0
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/index.js +9 -0
- package/dist/core/env-template.js +76 -3
- package/dist/core/run-tests.js +83 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { VERSION } from '../core/index.js';
|
|
18
18
|
import { runGenerate } from '../core/run-generate.js';
|
|
19
|
+
import { runTests } from '../core/run-tests.js';
|
|
19
20
|
import { runDiscover } from '../core/discover.js';
|
|
20
21
|
import { runMap } from '../core/map.js';
|
|
21
22
|
import { runInit } from '../core/init.js';
|
|
@@ -75,6 +76,7 @@ function printUsage() {
|
|
|
75
76
|
console.log(' testforge init Inspect project and write config');
|
|
76
77
|
console.log(' testforge update Re-check and update only what changed');
|
|
77
78
|
console.log(' testforge env Write .env.testing.example from docs');
|
|
79
|
+
console.log(' testforge run [--area a] [--journey j] Run the generated tests (Jest/Playwright)');
|
|
78
80
|
}
|
|
79
81
|
async function main() {
|
|
80
82
|
if (command === 'update') {
|
|
@@ -107,6 +109,13 @@ async function main() {
|
|
|
107
109
|
else if (command === 'env') {
|
|
108
110
|
runEnvTemplate('docs', '.env.testing.example', '.', generatedScanDirs());
|
|
109
111
|
}
|
|
112
|
+
else if (command === 'run') {
|
|
113
|
+
const code = runTests({
|
|
114
|
+
area: flagValue('--area'),
|
|
115
|
+
journey: flagValue('--journey'),
|
|
116
|
+
});
|
|
117
|
+
process.exitCode = code;
|
|
118
|
+
}
|
|
110
119
|
else if (command === 'generate') {
|
|
111
120
|
const useAI = !useBasic;
|
|
112
121
|
console.log(useAI
|
|
@@ -100,6 +100,76 @@ function scanSourceEnvVars(srcDir, opts = {}) {
|
|
|
100
100
|
walk(srcDir);
|
|
101
101
|
return vars;
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Find which of the repo's EXISTING .env* files already declare each var
|
|
105
|
+
* NAME (values are never read). Lets the template say "reuse from X" so a
|
|
106
|
+
* developer copies a real value instead of hunting for it.
|
|
107
|
+
*/
|
|
108
|
+
function scanExistingEnvFiles(root = '.', maxFiles = 3000) {
|
|
109
|
+
const found = new Map(); // var -> first file that has it
|
|
110
|
+
let seen = 0;
|
|
111
|
+
const walk = (dir) => {
|
|
112
|
+
if (seen >= maxFiles)
|
|
113
|
+
return;
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (seen >= maxFiles)
|
|
123
|
+
return;
|
|
124
|
+
const full = join(dir, entry.name);
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
if (SKIP_DIRS.has(entry.name) || entry.name === 'node_modules')
|
|
127
|
+
continue;
|
|
128
|
+
walk(full);
|
|
129
|
+
}
|
|
130
|
+
else if (entry.name.startsWith('.env') &&
|
|
131
|
+
!entry.name.endsWith('.example') &&
|
|
132
|
+
!entry.name.endsWith('.sample')) {
|
|
133
|
+
seen++;
|
|
134
|
+
try {
|
|
135
|
+
for (const line of readFileSync(full, 'utf-8').split('\n')) {
|
|
136
|
+
const m = line.match(/^\s*([A-Z][A-Z0-9_]*)\s*=/);
|
|
137
|
+
if (m && !found.has(m[1]))
|
|
138
|
+
found.set(m[1], full.replace(/^\.\//, ''));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// unreadable — ignore
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
walk(root);
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
150
|
+
/** A short "where do I get this" hint for one env var. */
|
|
151
|
+
function sourceHint(name, foundIn) {
|
|
152
|
+
const file = foundIn.get(name);
|
|
153
|
+
if (file)
|
|
154
|
+
return `reuse from ${file}`;
|
|
155
|
+
if (/DB_URL|MONGO|DATABASE|_URI/.test(name))
|
|
156
|
+
return 'test DB connection string (NOT prod)';
|
|
157
|
+
if (/STRIPE/.test(name))
|
|
158
|
+
return 'Stripe test-mode dashboard';
|
|
159
|
+
if (/TWILIO/.test(name))
|
|
160
|
+
return 'Twilio console';
|
|
161
|
+
if (/AWS_/.test(name))
|
|
162
|
+
return 'AWS IAM (test account)';
|
|
163
|
+
if (/REFRESH_TOKEN|LOGIN_(EMAIL|PASSWORD)/.test(name))
|
|
164
|
+
return 'ONE seed login for the qa- user';
|
|
165
|
+
if (/_TOKEN|_SECRET|_KEY|AUTH/.test(name))
|
|
166
|
+
return 'secret — service dashboard / team vault';
|
|
167
|
+
if (/_ID$/.test(name))
|
|
168
|
+
return 'a seeded/pre-created id';
|
|
169
|
+
if (/URL|HOST|PORT|BASE|DOMAIN/.test(name))
|
|
170
|
+
return 'service URL (local when running, or the env endpoint)';
|
|
171
|
+
return 'fill from your test environment';
|
|
172
|
+
}
|
|
103
173
|
/** Derive "area" and "journey" from a doc file name like web-payments.md */
|
|
104
174
|
function areaAndJourney(file) {
|
|
105
175
|
// file is a relative path like "api/payments.md" (folder = area).
|
|
@@ -174,6 +244,9 @@ export function runEnvTemplate(docsDir, outPath, srcDir = '.', testDirs = []) {
|
|
|
174
244
|
console.log('No environment variables found in the docs or source.');
|
|
175
245
|
return;
|
|
176
246
|
}
|
|
247
|
+
// Where each var can be sourced from (existing .env files win).
|
|
248
|
+
const foundIn = scanExistingEnvFiles();
|
|
249
|
+
const line = (v, used) => `${v}= # ${sourceHint(v, foundIn)} | used by: ${used}`;
|
|
177
250
|
// Build the file.
|
|
178
251
|
const lines = [
|
|
179
252
|
'# .env.testing.example',
|
|
@@ -189,7 +262,7 @@ export function runEnvTemplate(docsDir, outPath, srcDir = '.', testDirs = []) {
|
|
|
189
262
|
lines.push('# GENERATED SUITE (env the generated tests/testkit read)');
|
|
190
263
|
lines.push(`# ${'='.repeat(40)}`);
|
|
191
264
|
for (const v of suiteVars) {
|
|
192
|
-
lines.push(
|
|
265
|
+
lines.push(line(v, 'generated tests'));
|
|
193
266
|
}
|
|
194
267
|
lines.push('');
|
|
195
268
|
}
|
|
@@ -198,7 +271,7 @@ export function runEnvTemplate(docsDir, outPath, srcDir = '.', testDirs = []) {
|
|
|
198
271
|
lines.push('# APP (read directly from source — REQUIRED to boot)');
|
|
199
272
|
lines.push(`# ${'='.repeat(40)}`);
|
|
200
273
|
for (const v of bootVars) {
|
|
201
|
-
lines.push(
|
|
274
|
+
lines.push(line(v, 'app source'));
|
|
202
275
|
}
|
|
203
276
|
lines.push('');
|
|
204
277
|
}
|
|
@@ -209,7 +282,7 @@ export function runEnvTemplate(docsDir, outPath, srcDir = '.', testDirs = []) {
|
|
|
209
282
|
const areaMap = byArea.get(area);
|
|
210
283
|
for (const v of [...areaMap.keys()].sort()) {
|
|
211
284
|
const journeys = [...areaMap.get(v)].sort().join(', ');
|
|
212
|
-
lines.push(
|
|
285
|
+
lines.push(line(v, journeys));
|
|
213
286
|
}
|
|
214
287
|
lines.push('');
|
|
215
288
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testforge — run the generated tests.
|
|
3
|
+
*
|
|
4
|
+
* One command to actually EXECUTE what testforge generated, per area,
|
|
5
|
+
* with the right runner auto-detected (Jest for api/lambda, Playwright
|
|
6
|
+
* for web/ops) and the test env file loaded first. Saves the developer
|
|
7
|
+
* from remembering per-area commands, cwds, and env wiring.
|
|
8
|
+
*
|
|
9
|
+
* Precedence for the command run in each area:
|
|
10
|
+
* 1. config.testCommand[area] (explicit — wins)
|
|
11
|
+
* 2. a sensible default for the area's framework
|
|
12
|
+
* It runs from the area's nearest package root (where package.json /
|
|
13
|
+
* playwright.config live), with config.envFile merged into the env.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { join, dirname, resolve, relative } from 'node:path';
|
|
18
|
+
import { loadConfig, loadEnvFile, testDirForType, resolveFramework, } from './config.js';
|
|
19
|
+
/** Nearest ancestor dir containing a package.json (the runner's cwd). */
|
|
20
|
+
function packageRoot(dir) {
|
|
21
|
+
let d = resolve(dir);
|
|
22
|
+
for (;;) {
|
|
23
|
+
if (existsSync(join(d, 'package.json')))
|
|
24
|
+
return d;
|
|
25
|
+
const parent = dirname(d);
|
|
26
|
+
if (parent === d)
|
|
27
|
+
return resolve(dir); // no package.json found — use dir
|
|
28
|
+
d = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function runTests(opts = {}) {
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
const envFile = config.envFile ?? '.env.testing';
|
|
34
|
+
const envVars = loadEnvFile(envFile);
|
|
35
|
+
if (Object.keys(envVars).length > 0) {
|
|
36
|
+
console.log(`Loaded env from ${envFile} (${Object.keys(envVars).length} vars).`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.log(`No env loaded from "${envFile}" — copy .env.testing.example to it and fill values.`);
|
|
40
|
+
}
|
|
41
|
+
const env = { ...process.env, ...envVars };
|
|
42
|
+
const areas = Object.keys(config.testOutput ?? {}).filter((a) => !opts.area || a === opts.area);
|
|
43
|
+
if (areas.length === 0) {
|
|
44
|
+
console.log('No areas configured (testOutput is empty). Run `testforge init`.');
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
let anyFail = false;
|
|
48
|
+
let ranAnything = false;
|
|
49
|
+
for (const area of areas) {
|
|
50
|
+
const testDir = testDirForType(config, area);
|
|
51
|
+
if (!existsSync(testDir)) {
|
|
52
|
+
console.log(`[skip] ${area}: no tests at ${testDir}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const framework = resolveFramework(config, area, area);
|
|
56
|
+
const root = packageRoot(testDir);
|
|
57
|
+
const rel = relative(root, testDir) || '.';
|
|
58
|
+
let cmd = config.testCommand?.[area];
|
|
59
|
+
if (!cmd) {
|
|
60
|
+
if (framework === 'playwright') {
|
|
61
|
+
const target = opts.journey ? `${rel}/${opts.journey}` : rel;
|
|
62
|
+
cmd = `npx playwright test ${target}`;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
cmd = opts.journey
|
|
66
|
+
? `npx jest --testPathPattern "${opts.journey}"`
|
|
67
|
+
: `npx jest ${rel}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
ranAnything = true;
|
|
71
|
+
console.log(`\n=== ${area} (${framework}) ===`);
|
|
72
|
+
console.log(`$ ${cmd} [cwd: ${root}]`);
|
|
73
|
+
const res = spawnSync(cmd, { shell: true, cwd: root, env, stdio: 'inherit' });
|
|
74
|
+
if (res.status !== 0)
|
|
75
|
+
anyFail = true;
|
|
76
|
+
}
|
|
77
|
+
if (!ranAnything) {
|
|
78
|
+
console.log('\nNothing to run — generate tests first (`testforge generate`).');
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
console.log(anyFail ? '\nSome tests failed.' : '\nAll test runs passed.');
|
|
82
|
+
return anyFail ? 1 : 0;
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@activate-core/test-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "AI-powered QA automation. Scans a project, drafts journey docs, and generates seeded Jest/Playwright tests. Works in any project, with any AI CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|