@activate-core/test-forge 0.7.0 → 0.8.1
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/core/harness.js +55 -5
- package/dist/core/run-generate.js +54 -3
- package/package.json +1 -1
package/dist/core/harness.js
CHANGED
|
@@ -34,11 +34,37 @@ export const HARNESS_CONTRACT = [
|
|
|
34
34
|
` /collections + enums), returning the`,
|
|
35
35
|
` created ids/handles`,
|
|
36
36
|
` - TEST_IDS: object the stable well-known test ids/handles`,
|
|
37
|
+
` - setup(): Promise<void> THE one-call entry point (idempotent,`,
|
|
38
|
+
` safe to call from every beforeAll AND`,
|
|
39
|
+
` from a Playwright globalSetup). It:`,
|
|
40
|
+
` 1. connect()s + seed()s;`,
|
|
41
|
+
` 2. MINTS a fresh auth access token at`,
|
|
42
|
+
` runtime — read the project's REAL`,
|
|
43
|
+
` login/refresh auth (controllers/`,
|
|
44
|
+
` middleware) and either exchange a`,
|
|
45
|
+
` seed token (env, e.g. a refresh`,
|
|
46
|
+
` token) or log in the seeded qa-`,
|
|
47
|
+
` user to obtain it;`,
|
|
48
|
+
` 3. CACHES the minted token to a temp`,
|
|
49
|
+
` file so parallel workers reuse it`,
|
|
50
|
+
` (one mint per run, no churn);`,
|
|
51
|
+
` 4. POPULATES process.env for EVERY`,
|
|
52
|
+
` token/id env var the suite reads`,
|
|
53
|
+
` — GREP the sibling test files for`,
|
|
54
|
+
` process.env.*TOKEN*/*ID* names and`,
|
|
55
|
+
` set each to the minted/seeded`,
|
|
56
|
+
` value, so tests that read`,
|
|
57
|
+
` process.env work with NO hand-`,
|
|
58
|
+
` pasted tokens.`,
|
|
59
|
+
` - token(role?): string the current access token accessor`,
|
|
37
60
|
`Plus, for Playwright suites, ALSO export:`,
|
|
38
61
|
` - apiContext(request): a helper that returns an authed API request`,
|
|
39
|
-
` context (Bearer
|
|
62
|
+
` context (Bearer = the minted token — NEVER hardcode a secret)`,
|
|
40
63
|
` - login(page): Promise<void> drive/seed an authenticated UI session`,
|
|
41
64
|
`Use only env var NAMES for secrets/connection — never literal values.`,
|
|
65
|
+
`The ONLY credentials a developer should have to provide are the DB`,
|
|
66
|
+
`connection, a Stripe test key, and ONE seed/refresh token (or seeded`,
|
|
67
|
+
`login) — everything else setup() derives at runtime.`,
|
|
42
68
|
].join('\n');
|
|
43
69
|
/** Where an area's shared testkit lives, and how tests import it. */
|
|
44
70
|
export function harnessInfo(areaDir, framework, ext) {
|
|
@@ -64,8 +90,12 @@ function buildHarnessPrompt(harnessPath, framework) {
|
|
|
64
90
|
`the project's REAL data layer and auth.`,
|
|
65
91
|
``,
|
|
66
92
|
`You are inside the repository with read-only tools (Read, Grep, Glob,`,
|
|
67
|
-
`LS). READ the real DB config, the models/schemas,
|
|
68
|
-
`
|
|
93
|
+
`LS). READ the real DB config, the models/schemas, AND the real auth`,
|
|
94
|
+
`flow (login / refresh-token / OTP controllers + middleware) so setup()`,
|
|
95
|
+
`mints tokens the way the app actually does. Also GREP the sibling test`,
|
|
96
|
+
`files next to this testkit for every process.env.<NAME> they read`,
|
|
97
|
+
`(especially *TOKEN*, *_ID, base URLs) and make setup() POPULATE those`,
|
|
98
|
+
`exact names — the tests must run without any hand-pasted token.`,
|
|
69
99
|
``,
|
|
70
100
|
`This is a ${framework === 'jest' ? 'Jest/backend' : 'Playwright/UI'} area.`,
|
|
71
101
|
HARNESS_CONTRACT,
|
|
@@ -112,9 +142,10 @@ export function generateHarnessAsync(harnessPath, framework) {
|
|
|
112
142
|
/**
|
|
113
143
|
* A minimal Playwright config so a webapp/ops area that currently uses
|
|
114
144
|
* Cypress can actually run the generated specs. Points at the area's test
|
|
115
|
-
* dir
|
|
145
|
+
* dir, treats *.test.js as specs, and runs the testkit's setup() once via
|
|
146
|
+
* globalSetup (mints/populates tokens before any worker starts).
|
|
116
147
|
*/
|
|
117
|
-
export function playwrightConfig(testDirRelToPackage) {
|
|
148
|
+
export function playwrightConfig(testDirRelToPackage, globalSetupRelToPackage) {
|
|
118
149
|
return [
|
|
119
150
|
`// @generated by testforge`,
|
|
120
151
|
`// Minimal Playwright config so testforge's generated specs run.`,
|
|
@@ -124,6 +155,7 @@ export function playwrightConfig(testDirRelToPackage) {
|
|
|
124
155
|
`module.exports = defineConfig({`,
|
|
125
156
|
` testDir: '${testDirRelToPackage}',`,
|
|
126
157
|
` testMatch: /.*\\.(test|spec)\\.[jt]s$/,`,
|
|
158
|
+
` globalSetup: require.resolve('${globalSetupRelToPackage}'),`,
|
|
127
159
|
` timeout: 60000,`,
|
|
128
160
|
` use: {`,
|
|
129
161
|
` baseURL: process.env.BASE_URL || 'http://localhost:3000',`,
|
|
@@ -133,3 +165,21 @@ export function playwrightConfig(testDirRelToPackage) {
|
|
|
133
165
|
``,
|
|
134
166
|
].join('\n');
|
|
135
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Playwright globalSetup file: runs the testkit's one-call setup() once
|
|
170
|
+
* before the suite (connect + seed + mint/populate tokens). Sits next to
|
|
171
|
+
* the testkit and just delegates to it.
|
|
172
|
+
*/
|
|
173
|
+
export function globalSetupContent() {
|
|
174
|
+
return [
|
|
175
|
+
`// @generated by testforge`,
|
|
176
|
+
`// Runs once before the Playwright suite: seeds data and mints/`,
|
|
177
|
+
`// populates the auth tokens the specs read (via the testkit).`,
|
|
178
|
+
`const testkit = require('./testkit');`,
|
|
179
|
+
``,
|
|
180
|
+
`module.exports = async () => {`,
|
|
181
|
+
` await testkit.setup();`,
|
|
182
|
+
`};`,
|
|
183
|
+
``,
|
|
184
|
+
].join('\n');
|
|
185
|
+
}
|
|
@@ -25,7 +25,7 @@ import { generateWithAI, generateWithAIAsync } from './ai-generator.js';
|
|
|
25
25
|
import { generateSeed } from './seed-generator.js';
|
|
26
26
|
import { planSuite } from './suite-planner.js';
|
|
27
27
|
import { isSyntaxValid } from './validate.js';
|
|
28
|
-
import { harnessInfo, generateHarnessAsync, playwrightConfig, } from './harness.js';
|
|
28
|
+
import { harnessInfo, generateHarnessAsync, generateHarness, playwrightConfig, globalSetupContent, } from './harness.js';
|
|
29
29
|
import { hashContent, loadCache, saveCache } from './cache.js';
|
|
30
30
|
import { findMarkdownFiles } from './find-files.js';
|
|
31
31
|
import { loadConfig, testDirForType, resolveFramework } from './config.js';
|
|
@@ -140,6 +140,53 @@ function collisionWarning(testDir, id, ext) {
|
|
|
140
140
|
return null;
|
|
141
141
|
return `an existing test with different naming already covers this journey: ${clashes.join(', ')} — new file may duplicate it`;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* (Re)generate ONLY the shared harness for one or all areas — without
|
|
145
|
+
* touching any test file. Use this after upgrading the harness (e.g. to
|
|
146
|
+
* add token-minting): the new testkit populates process.env for the token
|
|
147
|
+
* vars the existing tests read, so they run unchanged. Cheap: one AI call
|
|
148
|
+
* per area, no test regeneration.
|
|
149
|
+
*/
|
|
150
|
+
export async function runHarness(only) {
|
|
151
|
+
const config = loadConfig();
|
|
152
|
+
const ext = config.testExtension ?? '.test.ts';
|
|
153
|
+
const areas = Object.keys(config.testOutput ?? {});
|
|
154
|
+
const written = [];
|
|
155
|
+
for (const area of areas) {
|
|
156
|
+
if (only && area !== only)
|
|
157
|
+
continue;
|
|
158
|
+
const areaDir = testDirForType(config, area);
|
|
159
|
+
const framework = resolveFramework(config, area, area);
|
|
160
|
+
const info = harnessInfo(areaDir, framework, ext);
|
|
161
|
+
let kit;
|
|
162
|
+
try {
|
|
163
|
+
kit = generateHarness(info.path, framework);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
console.log(` harness generation failed for area "${area}" — skipped`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!isSyntaxValid(kit, ext.endsWith('.js') ? '.js' : '.ts')) {
|
|
170
|
+
console.log(` harness for area "${area}" did not parse — skipped`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
mkdirSync(dirname(info.path), { recursive: true });
|
|
174
|
+
writeFileSync(info.path, kit, 'utf-8');
|
|
175
|
+
if (framework === 'playwright') {
|
|
176
|
+
const gsPath = join(info.dir, 'global-setup.js');
|
|
177
|
+
writeFileSync(gsPath, globalSetupContent(), 'utf-8');
|
|
178
|
+
const pkgRoot = dirname(areaDir);
|
|
179
|
+
const cfg = join(pkgRoot, 'playwright.config.js');
|
|
180
|
+
if (!existsSync(cfg)) {
|
|
181
|
+
const rel = areaDir.slice(pkgRoot.length + 1) || '.';
|
|
182
|
+
const gsRel = gsPath.slice(pkgRoot.length + 1);
|
|
183
|
+
writeFileSync(cfg, playwrightConfig(`./${rel}`, `./${gsRel}`), 'utf-8');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
written.push(info.path);
|
|
187
|
+
}
|
|
188
|
+
return written;
|
|
189
|
+
}
|
|
143
190
|
/** Does this doc file / journey match a --journey selector? */
|
|
144
191
|
function matchesJourney(file, journeyId, sel) {
|
|
145
192
|
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
@@ -175,13 +222,17 @@ export async function runGenerate(docsDir, useAI = true, force = false, only) {
|
|
|
175
222
|
}
|
|
176
223
|
mkdirSync(dirname(info.path), { recursive: true });
|
|
177
224
|
writeFileSync(info.path, kit, 'utf-8');
|
|
178
|
-
// Playwright areas need a config so the specs can
|
|
225
|
+
// Playwright areas need a config + globalSetup so the specs can run
|
|
226
|
+
// AND get their tokens minted once before the suite.
|
|
179
227
|
if (framework === 'playwright') {
|
|
228
|
+
const gsPath = join(info.dir, 'global-setup.js');
|
|
229
|
+
writeFileSync(gsPath, globalSetupContent(), 'utf-8');
|
|
180
230
|
const pkgRoot = dirname(areaDir);
|
|
181
231
|
const cfg = join(pkgRoot, 'playwright.config.js');
|
|
182
232
|
if (!existsSync(cfg)) {
|
|
183
233
|
const rel = areaDir.slice(pkgRoot.length + 1) || '.';
|
|
184
|
-
|
|
234
|
+
const gsRel = gsPath.slice(pkgRoot.length + 1);
|
|
235
|
+
writeFileSync(cfg, playwrightConfig(`./${rel}`, `./${gsRel}`), 'utf-8');
|
|
185
236
|
}
|
|
186
237
|
}
|
|
187
238
|
harnessByArea.set(area, info.path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@activate-core/test-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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": {
|