@activate-core/test-forge 0.6.0 → 0.7.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 +24 -1
- package/dist/core/ai-generator.js +13 -3
- package/dist/core/env-template.js +31 -5
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -22,6 +22,8 @@ import { runInit } from '../core/init.js';
|
|
|
22
22
|
import { runSetup } from '../core/setup.js';
|
|
23
23
|
import { runUpdate } from '../core/update.js';
|
|
24
24
|
import { runEnvTemplate } from '../core/env-template.js';
|
|
25
|
+
import { loadConfig } from '../core/config.js';
|
|
26
|
+
import { dirname } from 'node:path';
|
|
25
27
|
const command = process.argv[2];
|
|
26
28
|
const useBasic = process.argv.includes('--basic');
|
|
27
29
|
const force = process.argv.includes('--force');
|
|
@@ -39,6 +41,22 @@ function flagValue(name) {
|
|
|
39
41
|
function sourceOptions() {
|
|
40
42
|
return { reference: flagValue('--reference'), source: flagValue('--source') };
|
|
41
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The output dirs to scan for env vars the GENERATED suite reads (test
|
|
46
|
+
* output dirs + their sibling helpers/ + the seed dir). Shared by the
|
|
47
|
+
* `generate` and `env` commands so the example always reflects the suite.
|
|
48
|
+
*/
|
|
49
|
+
function generatedScanDirs() {
|
|
50
|
+
const cfg = loadConfig();
|
|
51
|
+
const dirs = new Set();
|
|
52
|
+
for (const out of Object.values(cfg.testOutput ?? {})) {
|
|
53
|
+
dirs.add(out);
|
|
54
|
+
dirs.add(dirname(out));
|
|
55
|
+
}
|
|
56
|
+
if (cfg.seedOutput)
|
|
57
|
+
dirs.add(cfg.seedOutput);
|
|
58
|
+
return [...dirs];
|
|
59
|
+
}
|
|
42
60
|
function printUsage() {
|
|
43
61
|
console.log(`testforge v${VERSION}`);
|
|
44
62
|
console.log('');
|
|
@@ -87,7 +105,7 @@ async function main() {
|
|
|
87
105
|
console.log('then run `testforge generate`.');
|
|
88
106
|
}
|
|
89
107
|
else if (command === 'env') {
|
|
90
|
-
runEnvTemplate('docs', '.env.testing.example');
|
|
108
|
+
runEnvTemplate('docs', '.env.testing.example', '.', generatedScanDirs());
|
|
91
109
|
}
|
|
92
110
|
else if (command === 'generate') {
|
|
93
111
|
const useAI = !useBasic;
|
|
@@ -130,6 +148,11 @@ async function main() {
|
|
|
130
148
|
if (seedFailures > 0) {
|
|
131
149
|
console.log(`Warning: ${seedFailures} seed file(s) could not be generated — see notes above.`);
|
|
132
150
|
}
|
|
151
|
+
// Auto-refresh the env template so running `generate` on its own (not
|
|
152
|
+
// just `setup`) tells the developer exactly which credentials the
|
|
153
|
+
// generated suite needs — including vars the tests themselves read.
|
|
154
|
+
console.log('');
|
|
155
|
+
runEnvTemplate('docs', '.env.testing.example', '.', generatedScanDirs());
|
|
133
156
|
}
|
|
134
157
|
}
|
|
135
158
|
else {
|
|
@@ -49,6 +49,11 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath) {
|
|
|
49
49
|
const framework = isApi
|
|
50
50
|
? 'Jest (with supertest for HTTP) for an API/backend test'
|
|
51
51
|
: 'Playwright for a web/UI test';
|
|
52
|
+
// Pending-test syntax differs by runner: Jest has test.todo/it.skip;
|
|
53
|
+
// Playwright has NEITHER (test.todo throws) — it uses test.fixme/skip.
|
|
54
|
+
const pending = isApi
|
|
55
|
+
? 'test.todo(...) or it.skip(...)'
|
|
56
|
+
: 'test.fixme(title, async () => {}) or test.skip(title, async () => {}) — Playwright has NO test.todo/it.*';
|
|
52
57
|
const ext = config.testExtension ?? '.test.ts';
|
|
53
58
|
const isJs = ext.endsWith('.js');
|
|
54
59
|
const language = isJs
|
|
@@ -86,7 +91,7 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath) {
|
|
|
86
91
|
`assert that a vulnerability exists when the journey doc EXPLICITLY`,
|
|
87
92
|
`documents it as a known/accepted behavior. If you cannot tell from the`,
|
|
88
93
|
`journey or map whether a defense exists, write the check as`,
|
|
89
|
-
|
|
94
|
+
`${pending} with a "// TODO:" note — never assume a`,
|
|
90
95
|
`guard is missing and produce a failing test that falsely brands the`,
|
|
91
96
|
`product as vulnerable.`,
|
|
92
97
|
``,
|
|
@@ -98,9 +103,9 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath) {
|
|
|
98
103
|
`models it touches. Confirm the REAL endpoint paths, HTTP methods,`,
|
|
99
104
|
`request/response shapes, status codes, auth requirements, and function`,
|
|
100
105
|
`signatures from the code. Base every assertion on what the code`,
|
|
101
|
-
`actually does. Only fall back to
|
|
106
|
+
`actually does. Only fall back to a pending test (${pending}) when`,
|
|
102
107
|
`the behavior genuinely cannot be determined from the source — do not`,
|
|
103
|
-
`leave something
|
|
108
|
+
`leave something pending that a quick read of the code would answer.`,
|
|
104
109
|
`Do NOT write or edit any file; output only the test code to stdout.`,
|
|
105
110
|
``,
|
|
106
111
|
isApi
|
|
@@ -119,6 +124,11 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath) {
|
|
|
119
124
|
`the UI. Use placeholder selectors with a TODO comment where the`,
|
|
120
125
|
`real page structure is unknown — do not invent selectors as if`,
|
|
121
126
|
`they are real.`,
|
|
127
|
+
`CRITICAL Playwright API: import { test, expect } from`,
|
|
128
|
+
`'@playwright/test'. There is NO test.todo / it.* / describe.skip`,
|
|
129
|
+
`in Playwright — using them throws at collection and breaks the`,
|
|
130
|
+
`WHOLE file. For a pending case use test.fixme(title, async () =>`,
|
|
131
|
+
`{}) or test.skip(...); group with test.describe(...).`,
|
|
122
132
|
].join('\n'),
|
|
123
133
|
``,
|
|
124
134
|
...(testPath
|
|
@@ -55,7 +55,8 @@ const SOURCE_EXT = /\.(c|m)?[jt]sx?$/i;
|
|
|
55
55
|
* skipping vendored/build/test output, with a file cap so a huge repo
|
|
56
56
|
* can't stall the run.
|
|
57
57
|
*/
|
|
58
|
-
function scanSourceEnvVars(srcDir,
|
|
58
|
+
function scanSourceEnvVars(srcDir, opts = {}) {
|
|
59
|
+
const { includeTests = false, maxFiles = 4000 } = opts;
|
|
59
60
|
const vars = new Set();
|
|
60
61
|
let seen = 0;
|
|
61
62
|
const walk = (dir) => {
|
|
@@ -78,8 +79,10 @@ function scanSourceEnvVars(srcDir, maxFiles = 4000) {
|
|
|
78
79
|
walk(full);
|
|
79
80
|
}
|
|
80
81
|
else if (SOURCE_EXT.test(entry.name)) {
|
|
81
|
-
//
|
|
82
|
-
|
|
82
|
+
// App-source scan skips test files; the generated-suite scan
|
|
83
|
+
// (includeTests) WANTS them — that is where the tests' own env
|
|
84
|
+
// vars (tokens, ids, base URLs) live.
|
|
85
|
+
if (!includeTests && /\.(test|spec)\./i.test(entry.name))
|
|
83
86
|
continue;
|
|
84
87
|
seen++;
|
|
85
88
|
try {
|
|
@@ -108,7 +111,7 @@ function areaAndJourney(file) {
|
|
|
108
111
|
// Fallback: flat file with no area folder.
|
|
109
112
|
return { area: 'general', journey: noExt };
|
|
110
113
|
}
|
|
111
|
-
export function runEnvTemplate(docsDir, outPath, srcDir = '.') {
|
|
114
|
+
export function runEnvTemplate(docsDir, outPath, srcDir = '.', testDirs = []) {
|
|
112
115
|
if (!existsSync(docsDir)) {
|
|
113
116
|
console.log(`No docs found at "${docsDir}". Run \`testforge setup\` first.`);
|
|
114
117
|
return;
|
|
@@ -153,7 +156,21 @@ export function runEnvTemplate(docsDir, outPath, srcDir = '.') {
|
|
|
153
156
|
}
|
|
154
157
|
const sourceVars = scanSourceEnvVars(srcDir);
|
|
155
158
|
const bootVars = [...sourceVars].filter((v) => !knownFromDocs.has(v)).sort();
|
|
156
|
-
|
|
159
|
+
// Vars the GENERATED suite (tests + seeds + testkit) actually reads —
|
|
160
|
+
// scanned from the output dirs, INCLUDING test files. This is what
|
|
161
|
+
// captures tokens/ids/base-URLs the generated code needs but that no
|
|
162
|
+
// doc/source mentioned.
|
|
163
|
+
const suiteScan = new Set();
|
|
164
|
+
for (const d of testDirs) {
|
|
165
|
+
for (const v of scanSourceEnvVars(d, { includeTests: true })) {
|
|
166
|
+
suiteScan.add(v);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const alreadyShown = new Set([...knownFromDocs, ...bootVars]);
|
|
170
|
+
const suiteVars = [...suiteScan]
|
|
171
|
+
.filter((v) => !alreadyShown.has(v))
|
|
172
|
+
.sort();
|
|
173
|
+
if (byArea.size === 0 && bootVars.length === 0 && suiteVars.length === 0) {
|
|
157
174
|
console.log('No environment variables found in the docs or source.');
|
|
158
175
|
return;
|
|
159
176
|
}
|
|
@@ -167,6 +184,15 @@ export function runEnvTemplate(docsDir, outPath, srcDir = '.') {
|
|
|
167
184
|
'# Keep .env.testing OUT of version control.',
|
|
168
185
|
'',
|
|
169
186
|
];
|
|
187
|
+
if (suiteVars.length > 0) {
|
|
188
|
+
lines.push(`# ${'='.repeat(40)}`);
|
|
189
|
+
lines.push('# GENERATED SUITE (env the generated tests/testkit read)');
|
|
190
|
+
lines.push(`# ${'='.repeat(40)}`);
|
|
191
|
+
for (const v of suiteVars) {
|
|
192
|
+
lines.push(`${v}= # used by: generated tests`);
|
|
193
|
+
}
|
|
194
|
+
lines.push('');
|
|
195
|
+
}
|
|
170
196
|
if (bootVars.length > 0) {
|
|
171
197
|
lines.push(`# ${'='.repeat(40)}`);
|
|
172
198
|
lines.push('# APP (read directly from source — REQUIRED to boot)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@activate-core/test-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|