@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.
@@ -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
- const fw = config.framework?.[journey.type];
21
- const isApi = journey.type === 'api' || fw === 'jest' || fw === 'supertest';
22
- const framework = fw
23
- ? `${fw} for a "${journey.type}" test`
24
- : isApi
25
- ? 'Jest (with supertest for HTTP) for an API/backend test'
26
- : 'an appropriate test framework for this kind of journey';
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
@@ -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
@@ -4,4 +4,4 @@
4
4
  * Currently exposes the package version only.
5
5
  * Model, parser, and engines will be added in upcoming steps.
6
6
  */
7
- export const VERSION = '0.3.0';
7
+ export const VERSION = '0.4.0';
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
- `- framework: an object mapping each area to its test framework`,
33
- ` (e.g. { "api": "jest", "web": "playwright", "ops": "playwright" }),`,
34
- ` based on what the project already uses for that area.`,
35
- ` the api tests, e.g. "packages/api/test/helpers/seed").`,
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`,
@@ -65,8 +65,14 @@ function parseField(line) {
65
65
  const required = parts.includes('required');
66
66
  return { name, type, required };
67
67
  }
68
- /** Parse a full markdown string into a Journey object. */
69
- export function parseJourney(markdown) {
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,
@@ -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, withEditTools } from './config.js';
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 } = withEditTools(splitCommand(loadConfig().aiCommand));
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(markdown, useAI, ext, testPath) {
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
- const journey = parseJourney(markdown);
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.type);
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(markdown, useAI, ext, testPath);
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.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": {