@activate-core/test-forge 0.3.0 → 0.4.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/core/ai-generator.js +12 -7
- package/dist/core/config.js +29 -0
- package/dist/core/index.js +1 -1
- package/dist/core/init.js +11 -4
- package/dist/core/parser.js +9 -2
- package/dist/core/reference.js +3 -3
- package/dist/core/run-generate.js +9 -5
- package/package.json +1 -1
|
@@ -17,13 +17,18 @@ const MAP_PATH = 'testforge.md';
|
|
|
17
17
|
/** Build the prompt we send to the AI for one journey. */
|
|
18
18
|
export function buildPrompt(journey, map, testPath) {
|
|
19
19
|
const config = loadConfig();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
// Opinionated: testforge only generates Jest (api/backend) or Playwright
|
|
21
|
+
// (web/UI) tests. We look the framework up by AREA, but clamp anything
|
|
22
|
+
// that is not jest/supertest/playwright (e.g. a project's cypress setup)
|
|
23
|
+
// to the sensible default for the journey's Type — so no other framework
|
|
24
|
+
// ever leaks into generated output.
|
|
25
|
+
const configured = (config.framework?.[journey.area] ?? '').toLowerCase();
|
|
26
|
+
const isApi = configured === 'jest' ||
|
|
27
|
+
configured === 'supertest' ||
|
|
28
|
+
(configured !== 'playwright' && journey.type === 'api');
|
|
29
|
+
const framework = isApi
|
|
30
|
+
? 'Jest (with supertest for HTTP) for an API/backend test'
|
|
31
|
+
: 'Playwright for a web/UI test';
|
|
27
32
|
const ext = config.testExtension ?? '.test.ts';
|
|
28
33
|
const isJs = ext.endsWith('.js');
|
|
29
34
|
const language = isJs
|
package/dist/core/config.js
CHANGED
|
@@ -127,6 +127,35 @@ export function withEditTools(cmd) {
|
|
|
127
127
|
args: [...cmd.args, '--permission-mode', 'acceptEdits'],
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Tools an external reference may need to be read: fetching a web page or
|
|
132
|
+
* a Notion doc. `acceptEdits` only auto-approves file edits, so a headless
|
|
133
|
+
* `claude -p` would otherwise be DENIED web/connector tools (no prompt to
|
|
134
|
+
* approve in non-interactive mode). We therefore pre-approve them.
|
|
135
|
+
*
|
|
136
|
+
* The connector must still be configured and authenticated in the user's
|
|
137
|
+
* own Claude CLI — this only grants permission to USE it headlessly. Both
|
|
138
|
+
* common Notion tool names are allow-listed (the claude.ai connector
|
|
139
|
+
* `claude_ai_Notion`, and a CLI-added server conventionally named
|
|
140
|
+
* `notion`). Non-Claude CLIs get the command unchanged.
|
|
141
|
+
*/
|
|
142
|
+
export function withReferenceTools(cmd) {
|
|
143
|
+
if (!isClaude(cmd.program))
|
|
144
|
+
return cmd;
|
|
145
|
+
const allow = [
|
|
146
|
+
'Read',
|
|
147
|
+
'Glob',
|
|
148
|
+
'Grep',
|
|
149
|
+
'WebFetch',
|
|
150
|
+
'WebSearch',
|
|
151
|
+
'mcp__notion__*',
|
|
152
|
+
'mcp__claude_ai_Notion__*',
|
|
153
|
+
].join(' ');
|
|
154
|
+
return {
|
|
155
|
+
program: cmd.program,
|
|
156
|
+
args: [...cmd.args, '--permission-mode', 'acceptEdits', '--allowedTools', allow],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
130
159
|
/**
|
|
131
160
|
* Decide where a journey's test file should go, based on its type
|
|
132
161
|
* and the config. Falls back to "generated-tests/<area>" when the
|
package/dist/core/index.js
CHANGED
package/dist/core/init.js
CHANGED
|
@@ -29,10 +29,17 @@ export function buildInitPrompt() {
|
|
|
29
29
|
` Point each to where that area's tests belong (e.g. in a monorepo,`,
|
|
30
30
|
` api -> "packages/api/test", web -> "packages/webapp/tests", ops ->`,
|
|
31
31
|
` "packages/ops/tests"; in a simple app, one area mapping to "tests").`,
|
|
32
|
-
|
|
33
|
-
`
|
|
34
|
-
`
|
|
35
|
-
|
|
32
|
+
` For UI areas, use a plain tests folder (e.g. ".../tests" or`,
|
|
33
|
+
` ".../tests/e2e") — do NOT point them at a Cypress folder such as`,
|
|
34
|
+
` "cypress/integration", because testforge does not generate Cypress.`,
|
|
35
|
+
`- framework: an object mapping each area to its test framework.`,
|
|
36
|
+
` testforge ONLY generates two kinds of tests, so each value MUST be`,
|
|
37
|
+
` exactly "jest" or "playwright" — never "cypress", "mocha", "vitest",`,
|
|
38
|
+
` or anything else, EVEN IF the project currently uses one of those.`,
|
|
39
|
+
` Choose the closest fit: backend / API / serverless / lambda / worker`,
|
|
40
|
+
` areas -> "jest"; UI / web / browser / admin-panel areas ->`,
|
|
41
|
+
` "playwright". Example: { "api": "jest", "web": "playwright", "ops":`,
|
|
42
|
+
` "playwright", "lambda": "jest" }.`,
|
|
36
43
|
`- testExtension: ".test.ts" or ".test.js" — match what existing`,
|
|
37
44
|
` tests use, or the project's language (TypeScript vs JavaScript).`,
|
|
38
45
|
`- aiCommand: keep it as "claude -p" unless there is a clear reason`,
|
package/dist/core/parser.js
CHANGED
|
@@ -65,8 +65,14 @@ function parseField(line) {
|
|
|
65
65
|
const required = parts.includes('required');
|
|
66
66
|
return { name, type, required };
|
|
67
67
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Parse a full markdown string into a Journey object.
|
|
70
|
+
*
|
|
71
|
+
* `area` is the journey's docs subfolder (e.g. "api", "ops"), which drives
|
|
72
|
+
* output location and framework. When omitted (a journey read outside an
|
|
73
|
+
* area folder), it falls back to the parsed `Type:`.
|
|
74
|
+
*/
|
|
75
|
+
export function parseJourney(markdown, area) {
|
|
70
76
|
const lines = markdown.split('\n').map((l) => l.trim());
|
|
71
77
|
let name = '';
|
|
72
78
|
let type = 'web';
|
|
@@ -112,6 +118,7 @@ export function parseJourney(markdown) {
|
|
|
112
118
|
return {
|
|
113
119
|
id: toId(name),
|
|
114
120
|
name,
|
|
121
|
+
area: area ?? type,
|
|
115
122
|
type,
|
|
116
123
|
route,
|
|
117
124
|
coverage,
|
package/dist/core/reference.js
CHANGED
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
* and tuned for the Claude CLI.
|
|
23
23
|
*/
|
|
24
24
|
import { execFileSync } from 'node:child_process';
|
|
25
|
-
import { loadConfig, splitCommand,
|
|
25
|
+
import { loadConfig, splitCommand, withReferenceTools } from './config.js';
|
|
26
26
|
/** Where the comparison report is written. */
|
|
27
27
|
export const DIFF_PATH = 'testforge.diff.md';
|
|
28
28
|
const MAP_PATH = 'testforge.md';
|
|
29
|
-
/** Run the configured AI command with read/write/web access. */
|
|
29
|
+
/** Run the configured AI command with read/write/web (+ Notion) access. */
|
|
30
30
|
function runAI(prompt) {
|
|
31
|
-
const { program, args } =
|
|
31
|
+
const { program, args } = withReferenceTools(splitCommand(loadConfig().aiCommand));
|
|
32
32
|
const output = execFileSync(program, args, {
|
|
33
33
|
input: prompt,
|
|
34
34
|
encoding: 'utf-8',
|
|
@@ -56,8 +56,7 @@ function generateValidated(generate, ext) {
|
|
|
56
56
|
* syntax gate + retry) and falls back to the deterministic basic
|
|
57
57
|
* generator if AI output can't be made valid.
|
|
58
58
|
*/
|
|
59
|
-
function buildTest(
|
|
60
|
-
const journey = parseJourney(markdown);
|
|
59
|
+
function buildTest(journey, useAI, ext, testPath) {
|
|
61
60
|
if (useAI) {
|
|
62
61
|
const ai = generateValidated(() => generateWithAI(journey, testPath), ext);
|
|
63
62
|
if (ai !== null) {
|
|
@@ -116,9 +115,14 @@ export function runGenerate(docsDir, useAI = true, force = false) {
|
|
|
116
115
|
for (const file of files) {
|
|
117
116
|
const markdown = readFileSync(join(docsDir, file), 'utf-8');
|
|
118
117
|
const hash = hashContent(markdown);
|
|
119
|
-
|
|
118
|
+
// The area is the doc's top-level folder (api/web/ops/lambda/...). It
|
|
119
|
+
// decides where the test goes and which framework is used — NOT the
|
|
120
|
+
// "Type:" line, which only says UI-vs-endpoint. This keeps ops/lambda
|
|
121
|
+
// journeys in their own package instead of leaking into web/api.
|
|
122
|
+
const area = file.split(/[\\/]/)[0] || undefined;
|
|
123
|
+
const journey = parseJourney(markdown, area);
|
|
120
124
|
// Where this journey's test and seed go.
|
|
121
|
-
const testDir = testDirForType(config, journey.
|
|
125
|
+
const testDir = testDirForType(config, journey.area);
|
|
122
126
|
const testPath = join(testDir, `${journey.id}${ext}`);
|
|
123
127
|
const seedPath = join(seedDir, `${journey.id}${seedExt}`);
|
|
124
128
|
// Skip if unchanged.
|
|
@@ -137,7 +141,7 @@ export function runGenerate(docsDir, useAI = true, force = false) {
|
|
|
137
141
|
if (collision)
|
|
138
142
|
warnings.push(collision);
|
|
139
143
|
// 1. Test file (syntax-gated before writing).
|
|
140
|
-
const { content, status } = buildTest(
|
|
144
|
+
const { content, status } = buildTest(journey, useAI, ext, testPath);
|
|
141
145
|
mkdirSync(dirname(testPath), { recursive: true });
|
|
142
146
|
writeFileSync(testPath, content, 'utf-8');
|
|
143
147
|
// 2. Seed file (only when using AI — basic mode has no seed concept).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@activate-core/test-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|