@activate-core/test-forge 0.9.7 → 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/run-generate.js +55 -8
- package/package.json +1 -1
|
@@ -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": {
|