@activate-core/test-forge 0.5.0 → 0.5.1

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.
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { execFileSync, spawn } from 'node:child_process';
14
14
  import { readFileSync, existsSync } from 'node:fs';
15
- import { loadConfig, splitCommand, withReadOnlyTools } from './config.js';
15
+ import { loadConfig, splitCommand, withReadOnlyTools, resolveFramework, } from './config.js';
16
16
  const MAP_PATH = 'testforge.md';
17
17
  /**
18
18
  * Spawn a command, feed `input` on stdin, resolve with its stdout.
@@ -42,14 +42,10 @@ function runWithStdin(program, args, input) {
42
42
  export function buildPrompt(journey, map, testPath, focus) {
43
43
  const config = loadConfig();
44
44
  // Opinionated: testforge only generates Jest (api/backend) or Playwright
45
- // (web/UI) tests. We look the framework up by AREA, but clamp anything
46
- // that is not jest/supertest/playwright (e.g. a project's cypress setup)
47
- // to the sensible default for the journey's Type — so no other framework
48
- // ever leaks into generated output.
49
- const configured = (config.framework?.[journey.area] ?? '').toLowerCase();
50
- const isApi = configured === 'jest' ||
51
- configured === 'supertest' ||
52
- (configured !== 'playwright' && journey.type === 'api');
45
+ // (web/UI) tests — any other framework (e.g. a project's Cypress setup)
46
+ // is clamped to the right one for the journey. Single source of truth in
47
+ // resolveFramework, shared with the output-layout decision.
48
+ const isApi = resolveFramework(config, journey.area, journey.type) === 'jest';
53
49
  const framework = isApi
54
50
  ? 'Jest (with supertest for HTTP) for an API/backend test'
55
51
  : 'Playwright for a web/UI test';
@@ -99,6 +99,19 @@ export function splitCommand(command) {
99
99
  const args = tokens.slice(1);
100
100
  return { program, args };
101
101
  }
102
+ /**
103
+ * Resolve the test framework for a journey — clamped to the only two
104
+ * testforge supports. A web/UI journey is always Playwright (even if the
105
+ * project uses Cypress); an endpoint journey is Jest. This single source
106
+ * of truth drives BOTH the generation prompt and the output layout.
107
+ */
108
+ export function resolveFramework(config, area, type) {
109
+ const configured = (config.framework?.[area] ?? '').toLowerCase();
110
+ const isApi = configured === 'jest' ||
111
+ configured === 'supertest' ||
112
+ (configured !== 'playwright' && type === 'api');
113
+ return isApi ? 'jest' : 'playwright';
114
+ }
102
115
  /** Is this command Claude's CLI (so Claude-specific flags apply)? */
103
116
  function isClaude(program) {
104
117
  const base = program.split(/[\\/]/).pop() ?? program;
@@ -27,7 +27,7 @@ import { planSuite } from './suite-planner.js';
27
27
  import { isSyntaxValid } from './validate.js';
28
28
  import { hashContent, loadCache, saveCache } from './cache.js';
29
29
  import { findMarkdownFiles } from './find-files.js';
30
- import { loadConfig, testDirForType } from './config.js';
30
+ import { loadConfig, testDirForType, resolveFramework } from './config.js';
31
31
  const CACHE_PATH = '.testforge/cache.json';
32
32
  /** How many times to re-ask the AI when output won't parse. */
33
33
  const MAX_GEN_ATTEMPTS = 2;
@@ -172,9 +172,17 @@ export async function runGenerate(docsDir, useAI = true, force = false, only) {
172
172
  // --journey <x>: generate just the one matching journey.
173
173
  if (only && !matchesJourney(file, journey.id, only))
174
174
  continue;
175
- // Deep mode: each journey becomes a FOLDER of focused test files.
175
+ // Layout depends on the framework's test runner:
176
+ // - Playwright (web/ops) recurses, so each journey gets its own
177
+ // FOLDER of focused specs (testDir/<journey>/<slug>).
178
+ // - Jest is often pointed at a flat glob (e.g. the-seam's
179
+ // `**/service/*.test.js`, which does NOT recurse), so api/lambda
180
+ // files stay FLAT in the test dir, journey-prefixed to stay unique
181
+ // (testDir/<journey>-<slug>). Otherwise they'd silently not run.
176
182
  const areaDir = testDirForType(config, journey.area);
177
- const testDir = join(areaDir, journey.id);
183
+ const framework = resolveFramework(config, journey.area, journey.type);
184
+ const flat = framework === 'jest';
185
+ const testDir = flat ? areaDir : join(areaDir, journey.id);
178
186
  const seedPath = join(seedDir, `${journey.id}${seedExt}`);
179
187
  // Skip if unchanged.
180
188
  if (!force && cache[file] === hash) {
@@ -201,7 +209,11 @@ export async function runGenerate(docsDir, useAI = true, force = false, only) {
201
209
  // call is independent, so a 15-file journey finishes in ~4 waves
202
210
  // instead of 15 serial calls.
203
211
  const generated = await pool(planned, GEN_CONCURRENCY, async (pf) => {
204
- const filePath = join(testDir, `${pf.slug}${ext}`);
212
+ // Flat (jest): prefix the file with the journey so many journeys can
213
+ // share one dir without clashing. Folder (playwright): the journey
214
+ // folder already namespaces it, so the slug alone is enough.
215
+ const base = flat ? `${journey.id}-${pf.slug}` : pf.slug;
216
+ const filePath = join(testDir, `${base}${ext}`);
205
217
  let content;
206
218
  let status;
207
219
  if (useAI) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@activate-core/test-forge",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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": {