@activate-core/test-forge 0.19.1 → 0.19.2

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/README.md CHANGED
@@ -330,6 +330,11 @@ The test runner produces its own pass/fail report.
330
330
  added to them, never instead of them — so name only what is particular
331
331
  to your product. Every domain has something: a shipping carrier, a CMS,
332
332
  a ticketing or streaming backend, an ad network.
333
+ - **relatedDirs** — other repos this project depends on, read-only, e.g.
334
+ `["../backend"]` for a frontend whose API and database live next door.
335
+ Every AI phase may read them, so the map captures the real data layer and
336
+ tests can check what the API stores. A headless run cannot read outside
337
+ the project otherwise.
333
338
 
334
339
  ## Notes
335
340
 
@@ -26,7 +26,7 @@
26
26
  * into the output. See withTextOnlyTools / withEditTools.
27
27
  */
28
28
  import { readFileSync, existsSync } from 'node:fs';
29
- import { join } from 'node:path';
29
+ import { join, resolve } from 'node:path';
30
30
  const CONFIG_PATH = 'testforge.config.json';
31
31
  const DEFAULT_CONFIG = {
32
32
  aiCommand: 'claude -p',
@@ -146,25 +146,42 @@ export function withTextOnlyTools(cmd) {
146
146
  * assumptions: without source access the model leaves "endpoint shape
147
147
  * unconfirmed" TODOs; with it, those become real assertions.
148
148
  */
149
- export function withReadOnlyTools(cmd) {
149
+ export function withReadOnlyTools(cmd, relatedDirs = loadConfig().relatedDirs ?? []) {
150
150
  if (!isClaude(cmd.program))
151
151
  return cmd;
152
152
  return {
153
153
  program: cmd.program,
154
154
  // Space-separated allowlist; Write/Edit/Bash deliberately excluded.
155
- args: [...cmd.args, '--allowedTools', 'Read Grep Glob LS'],
155
+ args: [...cmd.args, '--allowedTools', 'Read Grep Glob LS', ...addDirArgs(relatedDirs)],
156
156
  };
157
157
  }
158
+ /**
159
+ * Read-only tools a headless exploring phase needs pre-approved. The model
160
+ * reaches for shell `grep`/`ls` as often as for the Grep tool; under
161
+ * `acceptEdits` alone those are DENIED (no one is there to approve), and
162
+ * it gives up on what it was mapping. Only commands that read are listed.
163
+ */
164
+ const EXPLORE_TOOLS = ['Read', 'Grep', 'Glob', 'LS', 'Bash(grep:*)', 'Bash(rg:*)', 'Bash(ls:*)', 'Bash(wc:*)'];
165
+ /** `--add-dir` per related repo: reads outside the project are otherwise denied. */
166
+ function addDirArgs(relatedDirs) {
167
+ return relatedDirs.flatMap((dir) => ['--add-dir', resolve(dir)]);
168
+ }
158
169
  /**
159
170
  * Args for a phase where the model must read/write files (init, map,
160
- * discover). Mirrors withTextOnlyTools but grants edit permission.
171
+ * discover). Mirrors withTextOnlyTools but grants edit permission, plus the
172
+ * read-only exploring tools and the related repos.
161
173
  */
162
- export function withEditTools(cmd) {
174
+ export function withEditTools(cmd, relatedDirs = loadConfig().relatedDirs ?? []) {
163
175
  if (!isClaude(cmd.program))
164
176
  return cmd;
165
177
  return {
166
178
  program: cmd.program,
167
- args: [...cmd.args, '--permission-mode', 'acceptEdits'],
179
+ args: [
180
+ ...cmd.args,
181
+ '--permission-mode', 'acceptEdits',
182
+ '--allowedTools', EXPLORE_TOOLS.join(' '),
183
+ ...addDirArgs(relatedDirs),
184
+ ],
168
185
  };
169
186
  }
170
187
  /**
@@ -179,13 +196,11 @@ export function withEditTools(cmd) {
179
196
  * `claude_ai_Notion`, and a CLI-added server conventionally named
180
197
  * `notion`). Non-Claude CLIs get the command unchanged.
181
198
  */
182
- export function withReferenceTools(cmd) {
199
+ export function withReferenceTools(cmd, relatedDirs = loadConfig().relatedDirs ?? []) {
183
200
  if (!isClaude(cmd.program))
184
201
  return cmd;
185
202
  const allow = [
186
- 'Read',
187
- 'Glob',
188
- 'Grep',
203
+ ...EXPLORE_TOOLS,
189
204
  'WebFetch',
190
205
  'WebSearch',
191
206
  'mcp__notion__*',
@@ -193,7 +208,12 @@ export function withReferenceTools(cmd) {
193
208
  ].join(' ');
194
209
  return {
195
210
  program: cmd.program,
196
- args: [...cmd.args, '--permission-mode', 'acceptEdits', '--allowedTools', allow],
211
+ args: [
212
+ ...cmd.args,
213
+ '--permission-mode', 'acceptEdits',
214
+ '--allowedTools', allow,
215
+ ...addDirArgs(relatedDirs),
216
+ ],
197
217
  };
198
218
  }
199
219
  /**
@@ -20,12 +20,23 @@
20
20
  import { execFileSync } from 'node:child_process';
21
21
  import { loadConfig, splitCommand, withEditTools } from './config.js';
22
22
  /** Build the prompt that asks the AI to produce the project map. */
23
- export function buildMapPrompt(srcDir) {
23
+ export function buildMapPrompt(srcDir, relatedDirs = []) {
24
+ const related = relatedDirs.length
25
+ ? [
26
+ `This project depends on related repos, which you may READ but must`,
27
+ `not change: ${relatedDirs.map((d) => `"${d}"`).join(', ')}. Journeys`,
28
+ `come only from "${srcDir}". When the data layer (database, models,`,
29
+ `enums) or the API this project calls lives in a related repo, map it`,
30
+ `from there, and name the repo it came from.`,
31
+ ``,
32
+ ]
33
+ : [];
24
34
  return [
25
35
  `You are a senior engineer mapping a codebase so a QA tool can`,
26
36
  `plan tests and seed test data. Read the source code under`,
27
37
  `"${srcDir}" and produce a SHORT, high-level map of the project.`,
28
38
  ``,
39
+ ...related,
29
40
  `The map has two parts: journeys and the data layer.`,
30
41
  ``,
31
42
  `PART 1 — Journeys.`,
@@ -79,8 +90,9 @@ export function buildMapPrompt(srcDir) {
79
90
  }
80
91
  /** Run the map builder via the configured AI command. */
81
92
  export function runMap(srcDir) {
82
- const prompt = buildMapPrompt(srcDir);
83
- const { program, args } = withEditTools(splitCommand(loadConfig().aiCommand));
93
+ const config = loadConfig();
94
+ const prompt = buildMapPrompt(srcDir, config.relatedDirs ?? []);
95
+ const { program, args } = withEditTools(splitCommand(config.aiCommand), config.relatedDirs ?? []);
84
96
  const output = execFileSync(program, args, {
85
97
  input: prompt,
86
98
  encoding: 'utf-8',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@activate-core/test-forge",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
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": {
@@ -14,7 +14,8 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "rm -rf dist && tsc",
17
- "dev": "tsc --watch"
17
+ "dev": "tsc --watch",
18
+ "test": "tsx --test test/*.test.ts"
18
19
  },
19
20
  "license": "MIT",
20
21
  "publishConfig": {