@activate-core/test-forge 1.0.2 → 1.0.4
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
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* (an auth-gated page can move unseen).
|
|
20
20
|
*/
|
|
21
21
|
import { VERSION } from '../core/index.js';
|
|
22
|
-
import { runGenerate
|
|
22
|
+
import { runGenerate } from '../core/generate/run-generate.js';
|
|
23
23
|
import { runTests } from '../core/run/run-tests.js';
|
|
24
24
|
import { runDiscover } from '../core/docs/discover.js';
|
|
25
25
|
import { runMap } from '../core/project/map.js';
|
|
@@ -79,7 +79,7 @@ function printUsage() {
|
|
|
79
79
|
console.log(' testforge generate --force Regenerate everything');
|
|
80
80
|
console.log(' testforge generate --no-wait Stop when the AI hits its usage limit (default: wait for the reset and resume)');
|
|
81
81
|
console.log(' testforge generate --no-verify Skip running each journey once after generating (default: run and repair)');
|
|
82
|
-
console.log(' testforge generate --journey <name> Generate just ONE journey'
|
|
82
|
+
console.log(' testforge generate --journey <name> Generate just ONE journey');
|
|
83
83
|
console.log(' testforge generate --area <area> Generate a whole area (e.g. api)');
|
|
84
84
|
console.log(' testforge discover Draft journey docs (asks: map or reference)');
|
|
85
85
|
console.log(' testforge discover --journey <name> Draft just ONE journey doc');
|
|
@@ -144,13 +144,6 @@ async function main() {
|
|
|
144
144
|
});
|
|
145
145
|
process.exitCode = code;
|
|
146
146
|
}
|
|
147
|
-
else if (command === 'generate' && process.argv.includes('--harness')) {
|
|
148
|
-
// Regenerate only the shared harness (testkit), leaving every test file
|
|
149
|
-
// as it is — e.g. after a TestForge upgrade changes what a harness does.
|
|
150
|
-
console.log('Regenerating the shared test harness...\n');
|
|
151
|
-
const written = await runHarness(flagValue('--area'));
|
|
152
|
-
console.log(written.length ? `Written:\n ${written.join('\n ')}` : 'No harness written.');
|
|
153
|
-
}
|
|
154
147
|
else if (command === 'generate') {
|
|
155
148
|
const useAI = !useBasic;
|
|
156
149
|
console.log(useAI
|
|
@@ -310,7 +310,7 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath, transpor
|
|
|
310
310
|
` Locate inside the journey's form/container, never page-wide by a`,
|
|
311
311
|
` bare id or label that another always-mounted component also uses.`,
|
|
312
312
|
`(15) NOTHING YOU ARE NOT TESTING MAY BLOCK YOU. Apps open their own`,
|
|
313
|
-
` pop-ups for a signed-in or new user (
|
|
313
|
+
` pop-ups for a signed-in or new user (promotions, cookie consent,`,
|
|
314
314
|
` onboarding tips, notification prompts). Use the harness's login /`,
|
|
315
315
|
` storage helpers that pre-suppress them; if one still opens, close it`,
|
|
316
316
|
` before you continue. A click intercepted by an overlay the test is`,
|
|
@@ -337,6 +337,10 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath, transpor
|
|
|
337
337
|
` (logged-in state, redirect, payment) is then tested for real. Put`,
|
|
338
338
|
` "(verification bypassed)" in that test's title. Wrong-code and`,
|
|
339
339
|
` expired-code tests do NOT bypass: the real server must refuse them.`,
|
|
340
|
+
`(20) CHECK STATE THROUGH WHAT THE HARNESS OFFERS. If the harness has`,
|
|
341
|
+
` no database connection (the data lives in another repository), prove`,
|
|
342
|
+
` persisted state through the app's own API — the list or detail`,
|
|
343
|
+
` endpoint the client reads — never by importing a database client.`,
|
|
340
344
|
`When rules conflict, prefer reporting LESS: a pending test costs a line`,
|
|
341
345
|
`in the report; a false failure costs the team a release.`,
|
|
342
346
|
``,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { execFileSync, spawn } from 'node:child_process';
|
|
15
15
|
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
16
17
|
import { loadConfig, splitCommand, withReadOnlyTools } from '../project/config.js';
|
|
17
18
|
import { cleanOutput } from './ai-generator.js';
|
|
18
19
|
const MAP_PATH = 'testforge.md';
|
|
@@ -183,50 +184,61 @@ export const HARNESS_CONTRACT = [
|
|
|
183
184
|
` cases pending — never assert a scope the token cannot actually prove.`,
|
|
184
185
|
``,
|
|
185
186
|
`RUN ENVIRONMENT — the suite must run on a fresh machine given only the`,
|
|
186
|
-
`env file
|
|
187
|
-
`
|
|
188
|
-
`
|
|
189
|
-
`
|
|
190
|
-
`
|
|
191
|
-
` <name> — run: npm i -D <name>"
|
|
192
|
-
`
|
|
193
|
-
` before the suite starts
|
|
194
|
-
`
|
|
195
|
-
`
|
|
196
|
-
`
|
|
197
|
-
`
|
|
198
|
-
`
|
|
199
|
-
`
|
|
200
|
-
`
|
|
201
|
-
`
|
|
202
|
-
`
|
|
203
|
-
`
|
|
204
|
-
`
|
|
205
|
-
`
|
|
206
|
-
`
|
|
187
|
+
`env file, in WHATEVER stack this project uses. Decide everything below`,
|
|
188
|
+
`from what you read in THIS project; nothing here names a stack.`,
|
|
189
|
+
` - PACKAGES: use Node built-ins, the test runner, and what the project`,
|
|
190
|
+
` already has installed. If reaching the project's data layer needs a`,
|
|
191
|
+
` client the project lacks (a driver for its database), require it`,
|
|
192
|
+
` lazily, throw "testkit: missing package <name> — run: npm i -D <name>"`,
|
|
193
|
+
` when absent, and list it in REQUIRED_PACKAGES (an array literal) so`,
|
|
194
|
+
` the runner can check before the suite starts. Never add a package`,
|
|
195
|
+
` for anything else.`,
|
|
196
|
+
` - DATA LAYER IN ANOTHER REPOSITORY: when this repository does not own`,
|
|
197
|
+
` its database (the project map or a related repo shows the data layer`,
|
|
198
|
+
` belongs to another codebase, e.g. a frontend talking to a separate`,
|
|
199
|
+
` API), do NOT connect to that database: no data-layer client, nothing`,
|
|
200
|
+
` in REQUIRED_PACKAGES. seed() prepares data and tests read state`,
|
|
201
|
+
` through the app's own API — log in with the env credentials and call`,
|
|
202
|
+
` the endpoints the client itself calls. A state only a direct database`,
|
|
203
|
+
` write could create is not seeded: the tests needing it skip with`,
|
|
204
|
+
` that reason (the repository that owns the data tests it).`,
|
|
205
|
+
` - NO OTHER RUNTIMES: never shell out to another language runtime, a`,
|
|
206
|
+
` framework CLI or a container tool — the test machine need not have`,
|
|
207
|
+
` them.`,
|
|
208
|
+
` - CREDENTIALS COME FROM THE ENV FILE: every role the suite logs in as`,
|
|
209
|
+
` (customer, staff, admin, second customer, …) is an account whose`,
|
|
210
|
+
` login identifier and password are read from the env file`,
|
|
211
|
+
` (TEST_<ROLE>_EMAIL / TEST_<ROLE>_PASSWORD, or the identifier the app`,
|
|
212
|
+
` logs in with). The testkit NEVER hashes or invents a password. The`,
|
|
213
|
+
` seed may fill in the profile data such an account needs (addresses,`,
|
|
214
|
+
` vehicles, settings) but never its credential.`,
|
|
215
|
+
` - EXTRA ACCOUNT STATES (unverified, banned, deleted, another role, an`,
|
|
216
|
+
` expired credential): create them the way the app itself would —`,
|
|
217
|
+
` through its API where one exists, otherwise through the data layer`,
|
|
218
|
+
` by COPYING the stored credential of an env-provided account onto the`,
|
|
219
|
+
` new record, then setting the state. Never compute a credential.`,
|
|
220
|
+
` - SEED THE DATA TESTS NEED: codes, links, other customers' records —`,
|
|
221
|
+
` seeded and exposed through TEST_IDS and the env setup() publishes,`,
|
|
222
|
+
` so in auto mode no test waits on hand-made data. DB assertions (rows`,
|
|
223
|
+
` created, state persisted) go through the same data-layer client.`,
|
|
207
224
|
` - LOG IN ONCE: mint each role's token once per run (globalSetup) and`,
|
|
208
|
-
` reuse
|
|
209
|
-
`
|
|
210
|
-
`
|
|
211
|
-
`
|
|
212
|
-
`
|
|
213
|
-
`
|
|
214
|
-
`
|
|
215
|
-
`
|
|
216
|
-
` onboarding, notification prompts) and have login(page) and any`,
|
|
217
|
-
` seeded browser storage pre-set the flags that suppress them, like a`,
|
|
218
|
-
` "prompt already shown" flag. Give the seeded user a first name so`,
|
|
219
|
-
` greetings render.`,
|
|
225
|
+
` reuse it; login(page) injects it. If the backend rate-limits and`,
|
|
226
|
+
` its limiter state lives in a store the testkit can reach in local`,
|
|
227
|
+
` mode, export resetRateLimits() that clears ONLY the limiter entries,`,
|
|
228
|
+
` gated exactly like reset(), and call it from setup().`,
|
|
229
|
+
` - SUPPRESS THE APP'S OWN POP-UPS: find, in the client, any dialog that`,
|
|
230
|
+
` opens by itself for a seeded role (promotions, consent, onboarding,`,
|
|
231
|
+
` permission prompts) and have login(page) and seeded browser storage`,
|
|
232
|
+
` pre-set the flags that suppress it.`,
|
|
220
233
|
` - VERIFICATION BYPASS (Playwright): when the app verifies by a code the`,
|
|
221
|
-
` test cannot read (
|
|
222
|
-
` bypassVerification(page, { url,
|
|
223
|
-
` \`url\` (the final verify/register call)
|
|
224
|
-
`
|
|
225
|
-
`
|
|
226
|
-
`
|
|
227
|
-
` the
|
|
228
|
-
`
|
|
229
|
-
` runs against the real backend.`,
|
|
234
|
+
` test cannot read (a code sent by message or email, a magic link, a`,
|
|
235
|
+
` third-party sign-in), export bypassVerification(page, { url, role }):`,
|
|
236
|
+
` route the request at \`url\` (the final verify/register call) and`,
|
|
237
|
+
` fulfil it with the app's REAL success response shape (read the`,
|
|
238
|
+
` server handler and the client that consumes it) carrying a REAL token`,
|
|
239
|
+
` for an env-provided account of that role, obtained through the real`,
|
|
240
|
+
` login. Only the code check is skipped; everything after it runs`,
|
|
241
|
+
` against the real backend.`,
|
|
230
242
|
``,
|
|
231
243
|
`CONNECTION LIFECYCLE — setup() must be safe to call repeatedly across`,
|
|
232
244
|
`suites, or one suite's teardown breaks the next (MongoNotConnectedError).`,
|
|
@@ -240,6 +252,21 @@ export const HARNESS_CONTRACT = [
|
|
|
240
252
|
` (Playwright globalTeardown / Jest globalTeardown) that runs ONCE after`,
|
|
241
253
|
` the whole run — never per-file.`,
|
|
242
254
|
].join('\n');
|
|
255
|
+
/**
|
|
256
|
+
* Stamp written into every generated testkit: the contract it was built
|
|
257
|
+
* against. When TestForge's contract changes (a fix to how harnesses log in,
|
|
258
|
+
* seed or bypass verification), an existing testkit no longer matches and
|
|
259
|
+
* `generate` rebuilds it by itself — nobody has to remember a command.
|
|
260
|
+
*/
|
|
261
|
+
export const HARNESS_STAMP = `testforge-harness-contract: ${createHash('sha1').update(HARNESS_CONTRACT).digest('hex').slice(0, 12)}`;
|
|
262
|
+
/** Was this testkit built against the current contract? */
|
|
263
|
+
export function harnessIsCurrent(source) {
|
|
264
|
+
return source.includes(HARNESS_STAMP);
|
|
265
|
+
}
|
|
266
|
+
/** Prefix a freshly generated testkit with the stamp. */
|
|
267
|
+
export function stampHarness(source) {
|
|
268
|
+
return `// ${HARNESS_STAMP}\n${source}`;
|
|
269
|
+
}
|
|
243
270
|
/** Where an area's shared testkit lives, and how tests import it. */
|
|
244
271
|
export function harnessInfo(areaDir, framework, ext) {
|
|
245
272
|
// Jest tests are flat inside <areaDir> (e.g. .../test/service), so the
|
|
@@ -33,7 +33,7 @@ import { groundJourney } from '../docs/grounding.js';
|
|
|
33
33
|
import { generateSeed } from './seed-generator.js';
|
|
34
34
|
import { isSyntaxValid, checkTestGates } from '../gates/validate.js';
|
|
35
35
|
import { installedModules, transportHint, projectSelectors } from '../project/detect.js';
|
|
36
|
-
import { harnessInfo, generateHarnessAsync, generateHarness, playwrightConfig, globalSetupContent, fixturesDoc, jestConfig, jestSetupContent, } from './harness.js';
|
|
36
|
+
import { harnessInfo, generateHarnessAsync, harnessIsCurrent, stampHarness, generateHarness, playwrightConfig, globalSetupContent, fixturesDoc, jestConfig, jestSetupContent, } from './harness.js';
|
|
37
37
|
import { hashContent, loadCache, saveCache } from '../docs/cache.js';
|
|
38
38
|
import { findMarkdownFiles } from '../find-files.js';
|
|
39
39
|
import { loadConfig, testDirForType, resolveFramework } from '../project/config.js';
|
|
@@ -160,7 +160,7 @@ export async function runHarness(only) {
|
|
|
160
160
|
continue;
|
|
161
161
|
}
|
|
162
162
|
mkdirSync(dirname(info.path), { recursive: true });
|
|
163
|
-
writeFileSync(info.path, kit, 'utf-8');
|
|
163
|
+
writeFileSync(info.path, stampHarness(kit), 'utf-8');
|
|
164
164
|
writeFileSync(join(info.dir, 'FIXTURES.md'), fixturesDoc(), 'utf-8');
|
|
165
165
|
writeHarnessConfigs(framework, areaDir, info.dir, area);
|
|
166
166
|
written.push(info.path);
|
|
@@ -246,8 +246,14 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
|
|
|
246
246
|
// extra AI call (and no risk of that call eating the token limit).
|
|
247
247
|
// Pass --force to rebuild a stale testkit (e.g. after upgrading).
|
|
248
248
|
if (!force && existsSync(info.path)) {
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
if (harnessIsCurrent(readFileSync(info.path, 'utf-8'))) {
|
|
250
|
+
harnessByArea.set(area, info.path);
|
|
251
|
+
return info.path;
|
|
252
|
+
}
|
|
253
|
+
// Built by an older TestForge whose harness rules have since changed:
|
|
254
|
+
// rebuild it once here, so the fixes reach this project with no extra
|
|
255
|
+
// command. Test files are untouched.
|
|
256
|
+
console.log(` the ${area} test harness predates this TestForge version — rebuilding it`);
|
|
251
257
|
}
|
|
252
258
|
const kit = await generateValidatedAsync(() => generateHarnessAsync(info.path, framework), ext.endsWith('.js') ? '.js' : '.ts');
|
|
253
259
|
if (kit === null) {
|
|
@@ -255,7 +261,7 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
|
|
|
255
261
|
return undefined;
|
|
256
262
|
}
|
|
257
263
|
mkdirSync(dirname(info.path), { recursive: true });
|
|
258
|
-
writeFileSync(info.path, kit, 'utf-8');
|
|
264
|
+
writeFileSync(info.path, stampHarness(kit), 'utf-8');
|
|
259
265
|
writeFileSync(join(info.dir, 'FIXTURES.md'), fixturesDoc(), 'utf-8');
|
|
260
266
|
// Config + globalSetup (playwright) / config + setup (jest) so the specs run
|
|
261
267
|
// AND get their tokens minted once, plus the runner config scaffolding.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@activate-core/test-forge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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": {
|