@activate-core/test-forge 0.1.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/README.md +179 -0
- package/dist/cli/index.js +91 -0
- package/dist/core/ai-generator.js +141 -0
- package/dist/core/cache.js +105 -0
- package/dist/core/config.js +100 -0
- package/dist/core/discover.js +103 -0
- package/dist/core/env-template.js +107 -0
- package/dist/core/find-files.js +34 -0
- package/dist/core/generator.js +90 -0
- package/dist/core/index.js +7 -0
- package/dist/core/init.js +67 -0
- package/dist/core/map.js +90 -0
- package/dist/core/model.js +9 -0
- package/dist/core/parser.js +121 -0
- package/dist/core/playwright-writer.js +47 -0
- package/dist/core/run-generate.js +90 -0
- package/dist/core/security-payloads.js +24 -0
- package/dist/core/seed-generator.js +80 -0
- package/dist/core/setup.js +68 -0
- package/dist/core/update.js +52 -0
- package/package.json +27 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testforge — generate runner.
|
|
3
|
+
*
|
|
4
|
+
* For each journey markdown file in docsDir:
|
|
5
|
+
* - skips it if unchanged (hash cache), unless force = true
|
|
6
|
+
* - generates a TEST file (Jest for api, Playwright for web) via AI,
|
|
7
|
+
* falling back to the basic generator if AI fails
|
|
8
|
+
* - generates a SEED file for the journey's test data via AI
|
|
9
|
+
*
|
|
10
|
+
* Output locations come from config (testOutput per type, seedOutput),
|
|
11
|
+
* so tests/seeds land where the project expects them. Defaults to
|
|
12
|
+
* generated-tests/ when not configured.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
|
+
import { join, dirname } from 'node:path';
|
|
16
|
+
import { parseJourney } from './parser.js';
|
|
17
|
+
import { generateTestCases } from './generator.js';
|
|
18
|
+
import { writePlaywrightFile } from './playwright-writer.js';
|
|
19
|
+
import { generateWithAI } from './ai-generator.js';
|
|
20
|
+
import { generateSeed } from './seed-generator.js';
|
|
21
|
+
import { hashContent, loadCache, saveCache } from './cache.js';
|
|
22
|
+
import { findMarkdownFiles } from './find-files.js';
|
|
23
|
+
import { loadConfig, testDirForType } from './config.js';
|
|
24
|
+
const CACHE_PATH = '.testforge/cache.json';
|
|
25
|
+
/** Produce the test file content for one journey. */
|
|
26
|
+
function buildTest(markdown, useAI) {
|
|
27
|
+
const journey = parseJourney(markdown);
|
|
28
|
+
if (useAI) {
|
|
29
|
+
try {
|
|
30
|
+
return { content: generateWithAI(journey), status: 'ai' };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// fall through to basic
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const cases = generateTestCases(journey);
|
|
37
|
+
return { content: writePlaywrightFile(journey, cases), status: 'basic' };
|
|
38
|
+
}
|
|
39
|
+
export function runGenerate(docsDir, useAI = true, force = false) {
|
|
40
|
+
const files = findMarkdownFiles(docsDir);
|
|
41
|
+
if (files.length === 0) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
const ext = config.testExtension ?? '.test.ts';
|
|
46
|
+
const seedDir = config.seedOutput ?? 'generated-tests/seed';
|
|
47
|
+
const cache = loadCache(CACHE_PATH);
|
|
48
|
+
const results = [];
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
const markdown = readFileSync(join(docsDir, file), 'utf-8');
|
|
51
|
+
const hash = hashContent(markdown);
|
|
52
|
+
const journey = parseJourney(markdown);
|
|
53
|
+
// Where this journey's test and seed go.
|
|
54
|
+
const testDir = testDirForType(config, journey.type);
|
|
55
|
+
const testPath = join(testDir, `${journey.id}${ext}`);
|
|
56
|
+
const seedExt = (config.testExtension ?? '.test.ts').endsWith('.js')
|
|
57
|
+
? '.seed.js'
|
|
58
|
+
: '.seed.ts';
|
|
59
|
+
const seedPath = join(seedDir, `${journey.id}${seedExt}`);
|
|
60
|
+
// Skip if unchanged.
|
|
61
|
+
if (!force && cache[file] === hash) {
|
|
62
|
+
results.push({
|
|
63
|
+
journeyName: journey.name,
|
|
64
|
+
testPath,
|
|
65
|
+
seedPath,
|
|
66
|
+
status: 'skipped',
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// 1. Test file.
|
|
71
|
+
const { content, status } = buildTest(markdown, useAI);
|
|
72
|
+
mkdirSync(dirname(testPath), { recursive: true });
|
|
73
|
+
writeFileSync(testPath, content, 'utf-8');
|
|
74
|
+
// 2. Seed file (only when using AI — basic mode has no seed concept).
|
|
75
|
+
if (useAI) {
|
|
76
|
+
try {
|
|
77
|
+
const seed = generateSeed(markdown);
|
|
78
|
+
mkdirSync(dirname(seedPath), { recursive: true });
|
|
79
|
+
writeFileSync(seedPath, seed, 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// seed generation failed — leave a note but don't fail the run
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
cache[file] = hash;
|
|
86
|
+
results.push({ journeyName: journey.name, testPath, seedPath, status });
|
|
87
|
+
}
|
|
88
|
+
saveCache(CACHE_PATH, cache);
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testforge — security attack payloads.
|
|
3
|
+
*
|
|
4
|
+
* A small, curated list of common attack strings that a QA engineer
|
|
5
|
+
* would try against text-like input fields. Each entry has a name
|
|
6
|
+
* (what kind of attack) and a payload (the actual malicious input).
|
|
7
|
+
*
|
|
8
|
+
* Keeping these in one place makes it easy to add more later
|
|
9
|
+
* without touching the generator logic.
|
|
10
|
+
*/
|
|
11
|
+
export const SECURITY_PAYLOADS = [
|
|
12
|
+
{
|
|
13
|
+
name: 'XSS script tag',
|
|
14
|
+
payload: `<script>alert(1)</script>`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'SQL injection',
|
|
18
|
+
payload: `' OR 1=1 --`,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'HTML injection',
|
|
22
|
+
payload: `<img src=x onerror=alert(1)>`,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testforge — seed file generator (proof stage).
|
|
3
|
+
*
|
|
4
|
+
* Generates a test-data seed file for ONE journey. It feeds the AI:
|
|
5
|
+
* - the journey doc (what the feature does, its Data section)
|
|
6
|
+
* - the project map (testforge.md), which holds the data layer:
|
|
7
|
+
* database type, models/collections, key fields, enum values
|
|
8
|
+
*
|
|
9
|
+
* The AI writes a seed that inserts valid test records into the
|
|
10
|
+
* project's database, using the real schema it learned from the map.
|
|
11
|
+
* Nothing about a specific database is hardcoded here — the AI adapts
|
|
12
|
+
* to whatever the project uses (Mongo, SQL, etc.).
|
|
13
|
+
*
|
|
14
|
+
* Secrets are never embedded: the connection comes from the env var
|
|
15
|
+
* named in the map (e.g. process.env.DB_URL), not a literal string.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
19
|
+
import { loadConfig, splitCommand } from './config.js';
|
|
20
|
+
const MAP_PATH = 'testforge.md';
|
|
21
|
+
/** Build the seed-generation prompt for one journey. */
|
|
22
|
+
export function buildSeedPrompt(journeyDoc) {
|
|
23
|
+
const map = existsSync(MAP_PATH) ? readFileSync(MAP_PATH, 'utf-8') : '';
|
|
24
|
+
const ext = loadConfig().testExtension ?? '.test.ts';
|
|
25
|
+
const isJs = ext.endsWith('.js');
|
|
26
|
+
const language = isJs
|
|
27
|
+
? 'plain JavaScript (CommonJS: use require/module.exports, NO TypeScript type annotations)'
|
|
28
|
+
: 'TypeScript';
|
|
29
|
+
return [
|
|
30
|
+
`You are a senior SDET writing a TEST DATA SEED for one feature,`,
|
|
31
|
+
`so its tests have a known, valid starting state in the database.`,
|
|
32
|
+
``,
|
|
33
|
+
`Use the project map below to understand the data layer — the`,
|
|
34
|
+
`database type, the models/collections, their key fields, and the`,
|
|
35
|
+
`allowed enum values. Build records that are VALID against that`,
|
|
36
|
+
`schema (right collection/table, right fields, right enum values,`,
|
|
37
|
+
`correct relationships between records).`,
|
|
38
|
+
``,
|
|
39
|
+
`Requirements for the seed you write:`,
|
|
40
|
+
`- Connect using the env var named in the map (e.g.`,
|
|
41
|
+
` process.env.DB_URL). NEVER hardcode a connection string or any`,
|
|
42
|
+
` secret.`,
|
|
43
|
+
`- Make it IDEMPOTENT: use upsert by a stable, well-known test id so`,
|
|
44
|
+
` running it repeatedly (fresh DB or already-seeded) is safe.`,
|
|
45
|
+
`- Prefix test data clearly (e.g. names/ids starting with "qa-" or`,
|
|
46
|
+
` "QA-TEST") so it is obvious and easy to clean up.`,
|
|
47
|
+
`- Insert only what THIS feature's tests need as a precondition,`,
|
|
48
|
+
` honoring relationships (e.g. if an order references a user, seed`,
|
|
49
|
+
` the user too).`,
|
|
50
|
+
`- Use the database driver/ORM the project already uses (infer from`,
|
|
51
|
+
` the map / schema style).`,
|
|
52
|
+
``,
|
|
53
|
+
`Write the seed in ${language}.`,
|
|
54
|
+
`Output ONLY the seed code. No explanation, no markdown code fences.`,
|
|
55
|
+
`The very first characters must be code (a comment or import/require).`,
|
|
56
|
+
`Add a brief comment block at the top describing what it seeds.`,
|
|
57
|
+
`Where you must assume something not certain from the inputs,`,
|
|
58
|
+
`add a "// TODO:" note rather than inventing it confidently.`,
|
|
59
|
+
`--- PROJECT MAP (data layer) ---`,
|
|
60
|
+
map,
|
|
61
|
+
``,
|
|
62
|
+
`--- JOURNEY DOC ---`,
|
|
63
|
+
journeyDoc,
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
/** Generate a seed file's content for one journey doc, via the AI. */
|
|
67
|
+
export function generateSeed(journeyDoc) {
|
|
68
|
+
const prompt = buildSeedPrompt(journeyDoc);
|
|
69
|
+
const { program, args } = splitCommand(loadConfig().aiCommand);
|
|
70
|
+
const output = execFileSync(program, args, {
|
|
71
|
+
input: prompt,
|
|
72
|
+
encoding: 'utf-8',
|
|
73
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
74
|
+
});
|
|
75
|
+
// Strip code fences if the AI added them despite instructions.
|
|
76
|
+
return output
|
|
77
|
+
.replace(/^```[a-zA-Z]*\n/, '')
|
|
78
|
+
.replace(/\n```\s*$/, '')
|
|
79
|
+
.trim();
|
|
80
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testforge — setup command.
|
|
3
|
+
*
|
|
4
|
+
* Runs the one-time project preparation in order, with progress so the
|
|
5
|
+
* user can see what is happening at each stage:
|
|
6
|
+
* 1. init — inspect the project, write testforge.config.json
|
|
7
|
+
* 2. map — map journeys + data layer into testforge.md
|
|
8
|
+
* 3. discover — draft a doc per journey into docs/
|
|
9
|
+
*
|
|
10
|
+
* Each step is also available as its own command for re-running a
|
|
11
|
+
* single stage; setup just chains them for a smooth first run.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { hashProjectSource, loadCache, saveCache } from './cache.js';
|
|
15
|
+
import { runInit } from './init.js';
|
|
16
|
+
import { runMap } from './map.js';
|
|
17
|
+
import { runDiscover } from './discover.js';
|
|
18
|
+
import { runEnvTemplate } from './env-template.js';
|
|
19
|
+
export function runSetup(srcDir) {
|
|
20
|
+
// If the project already looks set up, don't redo it — point the user
|
|
21
|
+
// to `update` instead, which only refreshes what actually changed.
|
|
22
|
+
const alreadySetUp = existsSync('testforge.config.json') &&
|
|
23
|
+
existsSync('testforge.md') &&
|
|
24
|
+
existsSync('docs');
|
|
25
|
+
if (alreadySetUp) {
|
|
26
|
+
console.log('This project is already set up for testforge.');
|
|
27
|
+
console.log(' - testforge.config.json, testforge.md and docs/ exist.\n');
|
|
28
|
+
console.log("You don't need to run setup again. Instead:");
|
|
29
|
+
console.log(' testforge update refresh only what changed');
|
|
30
|
+
console.log(' testforge generate (re)generate tests from docs/');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log('Setting up testforge for this project.\n');
|
|
34
|
+
// Step 1 — config.
|
|
35
|
+
console.log('Step 1/3: Inspecting the project and writing config...');
|
|
36
|
+
runInit();
|
|
37
|
+
if (existsSync('testforge.config.json')) {
|
|
38
|
+
console.log(' done — testforge.config.json\n');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(' warning: testforge.config.json was not created\n');
|
|
42
|
+
}
|
|
43
|
+
// Step 2 — map.
|
|
44
|
+
console.log('Step 2/3: Mapping journeys and the data layer...');
|
|
45
|
+
runMap(srcDir);
|
|
46
|
+
if (existsSync('testforge.md')) {
|
|
47
|
+
console.log(' done — testforge.md\n');
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(' warning: testforge.md was not created\n');
|
|
51
|
+
}
|
|
52
|
+
// Step 3 — discover.
|
|
53
|
+
console.log('Step 3/3: Drafting a doc for each journey...');
|
|
54
|
+
runDiscover(srcDir, 'docs');
|
|
55
|
+
console.log(' done — docs/\n');
|
|
56
|
+
// Remember the source state so the first `update` can detect "no change".
|
|
57
|
+
const cache = loadCache('.testforge/cache.json');
|
|
58
|
+
cache['__source__'] = hashProjectSource(srcDir);
|
|
59
|
+
saveCache('.testforge/cache.json', cache);
|
|
60
|
+
// Build an env template so the developer knows which credentials each
|
|
61
|
+
// area/journey needs for running the tests.
|
|
62
|
+
console.log('Building env template...');
|
|
63
|
+
runEnvTemplate('docs', '.env.testing.example');
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log('Setup complete.');
|
|
66
|
+
console.log('Review the drafts in docs/, fix anything the AI got wrong,');
|
|
67
|
+
console.log('then run `testforge generate` to create tests and seed files.');
|
|
68
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testforge — update command.
|
|
3
|
+
*
|
|
4
|
+
* Refreshes a project that is already set up, doing only the work that
|
|
5
|
+
* is actually needed. It decides whether anything changed by hashing
|
|
6
|
+
* the project's SOURCE CODE (not the AI-generated map, which isn't
|
|
7
|
+
* deterministic). If the code is unchanged, it does nothing — no AI
|
|
8
|
+
* calls at all. If the code changed, it re-maps, re-discovers, and
|
|
9
|
+
* regenerates (generate still skips individual docs that didn't change).
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { runMap } from './map.js';
|
|
13
|
+
import { runDiscover } from './discover.js';
|
|
14
|
+
import { runGenerate } from './run-generate.js';
|
|
15
|
+
import { hashProjectSource, loadCache, saveCache } from './cache.js';
|
|
16
|
+
import { runEnvTemplate } from './env-template.js';
|
|
17
|
+
const CACHE_PATH = '.testforge/cache.json';
|
|
18
|
+
const SOURCE_KEY = '__source__';
|
|
19
|
+
export function runUpdate(srcDir) {
|
|
20
|
+
if (!existsSync('testforge.md') || !existsSync('testforge.config.json')) {
|
|
21
|
+
console.log("This project isn't set up yet.");
|
|
22
|
+
console.log('Run `testforge setup` first.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.log('Checking for changes in the source code...\n');
|
|
26
|
+
const cache = loadCache(CACHE_PATH);
|
|
27
|
+
const oldHash = cache[SOURCE_KEY];
|
|
28
|
+
const newHash = hashProjectSource(srcDir);
|
|
29
|
+
if (oldHash === newHash) {
|
|
30
|
+
console.log('No source changes detected. Everything is up to date.');
|
|
31
|
+
console.log('(Nothing regenerated — no AI calls made.)');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log('Source code changed — updating the project.\n');
|
|
35
|
+
console.log('Re-mapping the project...');
|
|
36
|
+
runMap(srcDir);
|
|
37
|
+
console.log(' done — testforge.md\n');
|
|
38
|
+
console.log('Updating journey docs...');
|
|
39
|
+
runDiscover(srcDir, 'docs');
|
|
40
|
+
console.log(' done — docs/\n');
|
|
41
|
+
console.log('Regenerating tests for changed journeys...');
|
|
42
|
+
const results = runGenerate('docs', true, false);
|
|
43
|
+
const made = results.filter((r) => r.status !== 'skipped').length;
|
|
44
|
+
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
45
|
+
console.log(` done — ${made} updated, ${skipped} unchanged.\n`);
|
|
46
|
+
cache[SOURCE_KEY] = newHash;
|
|
47
|
+
saveCache(CACHE_PATH, cache);
|
|
48
|
+
console.log('Refreshing env template...');
|
|
49
|
+
runEnvTemplate('docs', '.env.testing.example');
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log('Update complete.');
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@activate-core/test-forge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Markdown se automated tests banane wala QA tool. Kisi bhi project mein use ho sakta hai.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"testforge": "dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/core/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.0.0",
|
|
24
|
+
"tsx": "^4.22.4",
|
|
25
|
+
"typescript": "^5.4.0"
|
|
26
|
+
}
|
|
27
|
+
}
|