@activate-core/test-forge 0.18.2 → 0.18.3

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 CHANGED
@@ -175,9 +175,13 @@ async function main() {
175
175
  }
176
176
  }
177
177
  const generated = results.filter((r) => r.status !== 'skipped').length;
178
- const skipped = results.filter((r) => r.status === 'skipped').length;
178
+ // Rows with no test dir are notices (a non-journey file, the AI going
179
+ // away), not journeys that were unchanged — count them apart.
180
+ const notices = results.filter((r) => r.status === 'skipped' && !r.testDir).length;
181
+ const skipped = results.filter((r) => r.status === 'skipped' && r.testDir).length;
179
182
  const seedFailures = results.filter((r) => r.seedStatus === 'failed').length;
180
- console.log(`\nDone. ${generated} generated, ${skipped} skipped (unchanged).`);
183
+ console.log(`\nDone. ${generated} generated, ${skipped} skipped (unchanged)` +
184
+ (notices ? `, ${notices} notice(s) above.` : '.'));
181
185
  if (seedFailures > 0) {
182
186
  console.log(`Warning: ${seedFailures} seed file(s) could not be generated — see notes above.`);
183
187
  }
@@ -15,8 +15,9 @@
15
15
  * keeps this simple and works for projects of any size.
16
16
  */
17
17
  import { execFileSync } from 'node:child_process';
18
- import { existsSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
19
- import { join, relative, sep } from 'node:path';
18
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync, } from 'node:fs';
19
+ import { join, relative, sep, basename } from 'node:path';
20
+ import { notAJourneyDoc } from './parser.js';
20
21
  import { loadConfig, splitCommand, withEditTools } from './config.js';
21
22
  import { DEEP_DOC_GUIDANCE } from './doc-guidance.js';
22
23
  import { promptConfirm } from './prompt.js';
@@ -58,7 +59,10 @@ export function buildDiscoverPrompt(srcDir, docsDir, journey) {
58
59
  ` "${docsDir}/web/sign-in.md"`,
59
60
  ` "${docsDir}/ops/login.md"`,
60
61
  `Do NOT put area in the file name (no "api-payments.md") — the area`,
61
- `is the folder. Create the area subfolders as needed.`,
62
+ `is the folder. Create the area subfolders as needed. Write NOTHING at`,
63
+ `the root of "${docsDir}" and no file that is not a journey doc — a`,
64
+ `shared note that every journey should know about goes under`,
65
+ `"${docsDir}/_reference/" and is never a journey.`,
62
66
  ``,
63
67
  `Each file should be a RICH description of the journey. Start with`,
64
68
  `this header (required):`,
@@ -208,6 +212,27 @@ export async function runDiscover(srcDir, docsDir, opts = {}, journey) {
208
212
  else if (prior !== text)
209
213
  rewritten.push(path);
210
214
  }
215
+ // The prompt says "<area>/<journey>.md". The model still sometimes leaves a
216
+ // shared side-note at the docs root ("data traps every journey must know
217
+ // about"). Useful text, wrong place: generate would read it as a journey
218
+ // and build a suite named after the file. Move such files under
219
+ // _reference/ — kept, never a journey — and drop them from the record.
220
+ const quarantined = [];
221
+ for (const path of [...added]) {
222
+ const why = notAJourneyDoc(path, after.get(path));
223
+ if (!why)
224
+ continue;
225
+ const dest = join('_reference', basename(path));
226
+ mkdirSync(join(docsDir, '_reference'), { recursive: true });
227
+ renameSync(join(docsDir, path), join(docsDir, dest));
228
+ added.splice(added.indexOf(path), 1);
229
+ quarantined.push(`${path} -> ${dest} (${why})`);
230
+ }
231
+ if (quarantined.length) {
232
+ console.log(`\n${quarantined.length} file(s) were not journey docs and were moved under ` +
233
+ `${docsDir}/_reference/ (kept as notes, never generated from):\n ` +
234
+ quarantined.join('\n '));
235
+ }
211
236
  // Put back anything it reached for that this reference does not own. The
212
237
  // prompt asks it not to; this makes it true whether or not it complied.
213
238
  const restored = rewritten.filter((path) => wholeProject ? ownedElsewhere.has(path) : true);
@@ -65,6 +65,24 @@ function parseField(line) {
65
65
  const required = parts.includes('required');
66
66
  return { name, type, required };
67
67
  }
68
+ /**
69
+ * Is this file a journey doc at all? The layout discover promises — and every
70
+ * downstream step assumes — is "<area>/<journey>.md" with a "# Journey:"
71
+ * heading. Anything else under docs/ is reference material: a project's own
72
+ * notes checked in before TestForge arrived, or a shared side-note the AI wrote
73
+ * next to the journeys. Treating those as journeys produced test folders named
74
+ * after the file and suites with no name. Returns the reason when it is not.
75
+ */
76
+ export function notAJourneyDoc(relPath, markdown) {
77
+ const parts = relPath.split(/[\\/]/).filter(Boolean);
78
+ if (parts.length < 2)
79
+ return 'sits at the docs root, not in an area folder';
80
+ if (parts[0].startsWith('_'))
81
+ return `lives under ${parts[0]}/, which holds reference material`;
82
+ if (!/^#\s*Journey:/m.test(markdown))
83
+ return 'has no "# Journey:" heading';
84
+ return null;
85
+ }
68
86
  /**
69
87
  * Parse a full markdown string into a Journey object.
70
88
  *
@@ -175,7 +175,10 @@ export function buildReferenceDiscoverPrompt(srcDir, docsDir, reference, journey
175
175
  `- Where the reference is silent on a needed detail, add a "# TODO:"`,
176
176
  ` line describing what the developer must verify.`,
177
177
  `- Never include secrets — refer to env var names only.`,
178
- `- Create the area subfolders as needed.`,
178
+ `- Create the area subfolders as needed. Write NOTHING at the root of`,
179
+ ` "${docsDir}" and no file that is not a journey doc — a shared note`,
180
+ ` every journey should know about goes under "${docsDir}/_reference/"`,
181
+ ` and is never a journey.`,
179
182
  `- After creating the files, print the list of journeys you documented`,
180
183
  ` and remind the developer to review them.`,
181
184
  ].join('\n');
@@ -20,7 +20,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync
20
20
  import { join, dirname, basename, extname } from 'node:path';
21
21
  import { CACHE_PATH, COVERAGE_PATH, PLANS_DIR, STATE_DIR } from './paths.js';
22
22
  import { lintGenerated } from './lint.js';
23
- import { parseJourney } from './parser.js';
23
+ import { parseJourney, notAJourneyDoc } from './parser.js';
24
24
  import { generateTestCases } from './generator.js';
25
25
  import { writeDeferredStub } from './playwright-writer.js';
26
26
  import { generateWithAI, generateWithAIAsync, pingAi, } from './ai-generator.js';
@@ -374,8 +374,8 @@ function matchesJourney(file, journeyId, sel) {
374
374
  norm(stem).split('/').pop() === n);
375
375
  }
376
376
  export async function runGenerate(docsDir, useAI = true, force = false, only, areaOnly, opts = {}) {
377
- const files = findMarkdownFiles(docsDir);
378
- if (files.length === 0) {
377
+ const allFiles = findMarkdownFiles(docsDir);
378
+ if (allFiles.length === 0) {
379
379
  return [];
380
380
  }
381
381
  const config = loadConfig();
@@ -386,6 +386,34 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
386
386
  const results = [];
387
387
  lastAiFailure = null;
388
388
  aiUnavailable = null;
389
+ // Only journey docs are generated from. A file at the docs root, under a
390
+ // "_reference/" folder, or without a "# Journey:" heading is reference
391
+ // material — report it once and leave it alone rather than inventing a
392
+ // journey (and a test folder) out of it.
393
+ for (const file of allFiles) {
394
+ let why = null;
395
+ try {
396
+ why = notAJourneyDoc(file, readFileSync(join(docsDir, file), 'utf-8'));
397
+ }
398
+ catch {
399
+ why = 'could not be read';
400
+ }
401
+ if (!why)
402
+ continue;
403
+ results.push({
404
+ journeyName: file,
405
+ testDir: '',
406
+ testFiles: [],
407
+ seedPath: '',
408
+ status: 'skipped',
409
+ seedStatus: 'none',
410
+ warnings: [
411
+ `not a journey doc — it ${why}. Kept as reference material and ignored by generate. ` +
412
+ `Journeys live at ${docsDir}/<area>/<name>.md and open with "# Journey: …".`,
413
+ ],
414
+ });
415
+ }
416
+ const files = allFiles.filter((f) => !results.some((r) => r.journeyName === f));
389
417
  // Real UI test-hooks per area, extracted once from the components — feeds
390
418
  // the F10 selector-grounding gate so generated UI tests can only assert on
391
419
  // selectors that genuinely exist (never an invented data-testid).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@activate-core/test-forge",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
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": {