@activate-core/test-forge 1.0.3 → 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
|
|
@@ -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';
|
|
@@ -192,6 +193,15 @@ export const HARNESS_CONTRACT = [
|
|
|
192
193
|
` when absent, and list it in REQUIRED_PACKAGES (an array literal) so`,
|
|
193
194
|
` the runner can check before the suite starts. Never add a package`,
|
|
194
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).`,
|
|
195
205
|
` - NO OTHER RUNTIMES: never shell out to another language runtime, a`,
|
|
196
206
|
` framework CLI or a container tool — the test machine need not have`,
|
|
197
207
|
` them.`,
|
|
@@ -242,6 +252,21 @@ export const HARNESS_CONTRACT = [
|
|
|
242
252
|
` (Playwright globalTeardown / Jest globalTeardown) that runs ONCE after`,
|
|
243
253
|
` the whole run — never per-file.`,
|
|
244
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
|
+
}
|
|
245
270
|
/** Where an area's shared testkit lives, and how tests import it. */
|
|
246
271
|
export function harnessInfo(areaDir, framework, ext) {
|
|
247
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": {
|