@activate-core/test-forge 0.9.6 → 0.10.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 +10 -3
- package/dist/core/run-generate.js +55 -8
- package/package.json +1 -1
|
@@ -121,9 +121,16 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath) {
|
|
|
121
121
|
].join('\n')
|
|
122
122
|
: [
|
|
123
123
|
`Because this is a web journey, write Playwright tests that drive`,
|
|
124
|
-
`the UI.
|
|
125
|
-
`
|
|
126
|
-
`
|
|
124
|
+
`the UI. FIRST find the REAL selectors: with your read-only tools,`,
|
|
125
|
+
`locate and READ this page's actual React/Vue components (grep the`,
|
|
126
|
+
`webapp source for the route, page name, and on-screen text, then`,
|
|
127
|
+
`open those component files). Use the selectors that genuinely`,
|
|
128
|
+
`exist there — data-testid/data-cy, input name/id, aria-role,`,
|
|
129
|
+
`label text, or button text — via Playwright locators`,
|
|
130
|
+
`(getByRole/getByLabel/getByTestId/getByText). Only leave a`,
|
|
131
|
+
`"// TODO: confirm selector" when the element truly cannot be`,
|
|
132
|
+
`found in the component source; do NOT default everything to a`,
|
|
133
|
+
`TODO. Never invent a selector as if it were real.`,
|
|
127
134
|
`CRITICAL Playwright API: import { test, expect } from`,
|
|
128
135
|
`'@playwright/test'. There is NO test.todo / it.* / describe.skip`,
|
|
129
136
|
`in Playwright — using them throws at collection and breaks the`,
|
|
@@ -187,6 +187,36 @@ export async function runHarness(only) {
|
|
|
187
187
|
}
|
|
188
188
|
return written;
|
|
189
189
|
}
|
|
190
|
+
const PLAN_DIR = '.testforge/plans';
|
|
191
|
+
/**
|
|
192
|
+
* Get a journey's file plan, cached by doc hash. Caching the plan keeps
|
|
193
|
+
* the file slugs STABLE across runs, so per-file resume (skip-existing)
|
|
194
|
+
* actually matches — and it saves the planner AI call on a resume. When
|
|
195
|
+
* the doc changes, the hash changes and we re-plan.
|
|
196
|
+
*/
|
|
197
|
+
function loadOrPlan(journey, docHash) {
|
|
198
|
+
const p = join(PLAN_DIR, `${journey.area}__${journey.id}.json`);
|
|
199
|
+
if (existsSync(p)) {
|
|
200
|
+
try {
|
|
201
|
+
const cached = JSON.parse(readFileSync(p, 'utf-8'));
|
|
202
|
+
if (cached.hash === docHash && Array.isArray(cached.files) && cached.files.length) {
|
|
203
|
+
return cached.files;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// corrupt plan cache — re-plan
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const files = planSuite(journey);
|
|
211
|
+
try {
|
|
212
|
+
mkdirSync(PLAN_DIR, { recursive: true });
|
|
213
|
+
writeFileSync(p, JSON.stringify({ hash: docHash, files }, null, 2), 'utf-8');
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// best-effort cache
|
|
217
|
+
}
|
|
218
|
+
return files;
|
|
219
|
+
}
|
|
190
220
|
/** Does this doc file / journey match a --journey selector? */
|
|
191
221
|
function matchesJourney(file, journeyId, sel) {
|
|
192
222
|
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
@@ -301,7 +331,7 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
|
|
|
301
331
|
// 1. Plan the suite (deep mode), then generate one focused file per
|
|
302
332
|
// concern. In basic mode there is no AI planner — one file only.
|
|
303
333
|
const planned = useAI
|
|
304
|
-
?
|
|
334
|
+
? loadOrPlan(journey, hash)
|
|
305
335
|
: [{ slug: journey.id, title: journey.name, covers: '' }];
|
|
306
336
|
// Generate all focused files CONCURRENTLY (bounded pool). Each AI
|
|
307
337
|
// call is independent, so a 15-file journey finishes in ~4 waves
|
|
@@ -312,6 +342,20 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
|
|
|
312
342
|
// folder already namespaces it, so the slug alone is enough.
|
|
313
343
|
const base = flat ? `${journey.id}-${pf.slug}` : pf.slug;
|
|
314
344
|
const filePath = join(testDir, `${base}${ext}`);
|
|
345
|
+
// PER-FILE RESUME: if this file already exists and parses, keep it
|
|
346
|
+
// — don't spend tokens regenerating it. So a journey interrupted
|
|
347
|
+
// midway resumes from exactly the files it hadn't written yet.
|
|
348
|
+
// (--force ignores this and regenerates.)
|
|
349
|
+
if (!force && existsSync(filePath)) {
|
|
350
|
+
try {
|
|
351
|
+
if (isSyntaxValid(readFileSync(filePath, 'utf-8'), ext)) {
|
|
352
|
+
return { slug: pf.slug, path: filePath, status: 'ai' };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// unreadable — fall through and regenerate
|
|
357
|
+
}
|
|
358
|
+
}
|
|
315
359
|
let content;
|
|
316
360
|
let status;
|
|
317
361
|
if (useAI) {
|
|
@@ -331,14 +375,17 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
|
|
|
331
375
|
content = built.content;
|
|
332
376
|
status = 'basic';
|
|
333
377
|
}
|
|
334
|
-
|
|
378
|
+
// Write IMMEDIATELY (not batched at the end of the journey), so an
|
|
379
|
+
// interruption preserves every file generated so far.
|
|
380
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
381
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
382
|
+
return { slug: pf.slug, path: filePath, status };
|
|
335
383
|
});
|
|
336
|
-
const testFiles =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
384
|
+
const testFiles = generated.map((g) => ({
|
|
385
|
+
slug: g.slug,
|
|
386
|
+
path: g.path,
|
|
387
|
+
status: g.status,
|
|
388
|
+
}));
|
|
342
389
|
const anyAI = testFiles.some((t) => t.status === 'ai');
|
|
343
390
|
const anyBasic = testFiles.some((t) => t.status === 'basic');
|
|
344
391
|
const status = anyAI || !anyBasic ? 'ai' : 'basic';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@activate-core/test-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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": {
|