@activate-core/test-forge 0.19.3 → 1.0.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.
@@ -23,6 +23,7 @@ import { DEEP_DOC_GUIDANCE } from './doc-guidance.js';
23
23
  import { promptConfirm } from '../prompt.js';
24
24
  import { loadSourceChoice, resolveDocsSource, saveJourneyReferences, saveSourceChoice, } from './source-of-truth.js';
25
25
  import { analyzeReference, draftDocsFromReference, DIFF_PATH, } from './reference.js';
26
+ import { reviewDocs } from './review.js';
26
27
  /** Scope line: one journey, or all of them. */
27
28
  function scopeLines(journey) {
28
29
  return journey
@@ -180,7 +181,7 @@ export async function runDiscover(srcDir, docsDir, opts = {}, journey) {
180
181
  if (!ok) {
181
182
  console.log('Basing docs on testforge.md instead.\n');
182
183
  saveSourceChoice({ mode: 'map' });
183
- return runCodeDiscover(srcDir, docsDir, journey);
184
+ return discoverFromCode(srcDir, docsDir, journey);
184
185
  }
185
186
  }
186
187
  // Is this the product's specification, or one feature's? A ticket's PDF
@@ -246,6 +247,10 @@ export async function runDiscover(srcDir, docsDir, opts = {}, journey) {
246
247
  for (const path of restored) {
247
248
  writeFileSync(join(docsDir, path), before.get(path), 'utf-8');
248
249
  }
250
+ // Second, focused pass per written doc: the drafting pass covers every
251
+ // journey at once and drops the small scenarios.
252
+ const written = [...added, ...rewritten.filter((p) => !restored.includes(p))];
253
+ await reviewDocs(docsDir, written, srcDir, source.reference);
249
254
  if (wholeProject) {
250
255
  saveSourceChoice({ mode: 'reference', reference: source.reference });
251
256
  if (added.length)
@@ -270,5 +275,15 @@ export async function runDiscover(srcDir, docsDir, opts = {}, journey) {
270
275
  return out;
271
276
  }
272
277
  saveSourceChoice({ mode: 'map' });
273
- return runCodeDiscover(srcDir, docsDir, journey);
278
+ return discoverFromCode(srcDir, docsDir, journey);
279
+ }
280
+ /** Draft from the code, then review every doc the draft wrote or changed. */
281
+ async function discoverFromCode(srcDir, docsDir, journey) {
282
+ const before = snapshotDocs(docsDir);
283
+ const out = runCodeDiscover(srcDir, docsDir, journey);
284
+ const changed = [...snapshotDocs(docsDir)]
285
+ .filter(([path, text]) => before.get(path) !== text && !notAJourneyDoc(path, text))
286
+ .map(([path]) => path);
287
+ await reviewDocs(docsDir, changed, srcDir);
288
+ return out;
274
289
  }
@@ -81,6 +81,36 @@ export const DEEP_DOC_GUIDANCE = [
81
81
  ` failure is an inconsistency, not a spec defect. Never use [observed]`,
82
82
  ` to override or soften a [spec] criterion.`,
83
83
  ``,
84
+ `METHOD. Work through these steps for every journey, in order:`,
85
+ `1. CONTROLS. Start the doc with a "## Controls" table listing EVERY`,
86
+ ` interactive element and where it appears: each input, button, link,`,
87
+ ` checkbox, code box, dropdown, pop-up, menu item, and the page itself.`,
88
+ ` Secondary controls count as much as the main form: Resend, Back,`,
89
+ ` show/hide, close, apply, sign-out, provider buttons. Apply every`,
90
+ ` [baseline] item that fits to EACH row, not just to the main form.`,
91
+ `2. COMBINATIONS. When the source states a rule across alternatives and`,
92
+ ` conditions ("for login and sign-up, also on a slow connection and on`,
93
+ ` a retry"), write one criterion per combination (login × slow, login ×`,
94
+ ` retry, sign-up × slow, sign-up × retry), never just one of them.`,
95
+ `3. CODE BRANCHES. Read the code the journey runs — screens, components,`,
96
+ ` client API calls, and the server handlers they reach (related repos`,
97
+ ` included) — and walk EVERY validation rule, error response and`,
98
+ ` conditional branch: account states (unverified, banned, deleted,`,
99
+ ` other roles), limits and expiry, empty and failure states, refresh`,
100
+ ` and deep links, what is stored in the browser and when it is`,
101
+ ` cleared, input handling (paste, clearing after an error). Each branch`,
102
+ ` becomes an [observed] criterion, or a Question when only product can`,
103
+ ` say what it should do. A skipped branch is the commonest gap.`,
104
+ `4. QUOTE [spec]. End every [spec] criterion with the source sentence it`,
105
+ ` rests on, quoted: (source: "..."). A criterion you cannot quote is`,
106
+ ` NOT [spec] — make it [baseline] or [observed], or a Question.`,
107
+ `5. SHARED RULES. A rule shared by several journeys (password, redirects,`,
108
+ ` attack payloads, accepted formats) uses the same cases and values in`,
109
+ ` every doc.`,
110
+ `6. COVERAGE. After the numbered sections, add "## Coverage": one row per`,
111
+ ` control from step 1, naming the section that covers each [baseline]`,
112
+ ` item that fits it. A blank cell is a gap — fill it before finishing.`,
113
+ ``,
84
114
  `TEST DATA. Tests fill data themselves. Never ask for an exact value:`,
85
115
  `valid data comes from the stated or [observed] rules; invalid data`,
86
116
  `breaks one rule at a time; a choice control is filled with an option`,
@@ -100,6 +130,7 @@ export const DEEP_DOC_GUIDANCE = [
100
130
  `Organise the body with numbered "## N. <Section>" headings, ONE`,
101
131
  `concern per section, because each section later becomes its own`,
102
132
  `focused test file — so the richer and better-separated the sections,`,
103
- `the better the test suite. Put "## Test setup" and "## Questions for`,
104
- `product (not asserted)" last, without numbers.`,
133
+ `the better the test suite. "## Controls" comes first; "## Coverage",`,
134
+ `"## Test setup" and "## Questions for product (not asserted)" come`,
135
+ `last. None of these four is numbered.`,
105
136
  ].join('\n');
@@ -0,0 +1,81 @@
1
+ /**
2
+ * testforge — journey doc gap review.
3
+ *
4
+ * One drafting pass writes every journey at once, and it reliably runs out
5
+ * of attention before the small things: a Resend button with no failure
6
+ * cases, a code box nobody pasted into, one combination of the four the
7
+ * source asked for, a server branch never read. So each freshly written doc
8
+ * gets a second, focused pass of its own: re-run the method from
9
+ * DEEP_DOC_GUIDANCE against that one journey, fill what is missing, and fix
10
+ * tiers that claim more than their source says. The reviewer edits the file
11
+ * in place and never deletes a sound criterion.
12
+ */
13
+ import { join } from 'node:path';
14
+ import { loadConfig, splitCommand, withEditTools, withReferenceTools } from '../project/config.js';
15
+ import { runWithStdin } from '../generate/ai-generator.js';
16
+ import { pool } from '../generate/run-generate.js';
17
+ import { DEEP_DOC_GUIDANCE } from './doc-guidance.js';
18
+ /** Build the prompt that reviews and completes ONE journey doc. */
19
+ export function buildReviewPrompt(docPath, srcDir, reference) {
20
+ const source = reference
21
+ ? `The source of truth is the reference: ${reference}. Read it fully; [spec]` +
22
+ ` criteria come only from it, and the code under "${srcDir}" (and any` +
23
+ ` related repos) supplies technical detail and [observed] rules.`
24
+ : `The source of truth is the code under "${srcDir}" (and any related repos).`;
25
+ return [
26
+ `You are a senior QA engineer reviewing ONE journey doc that a first`,
27
+ `pass drafted: "${docPath}". Your job is to make it COMPLETE, so that`,
28
+ `no scenario a careful QA engineer would test is missing.`,
29
+ ``,
30
+ source,
31
+ ``,
32
+ `The doc must follow this guidance:`,
33
+ DEEP_DOC_GUIDANCE,
34
+ ``,
35
+ `Review it step by step against the METHOD above and fix what you find:`,
36
+ `- Controls: is every interactive element of this journey in the table,`,
37
+ ` secondary controls included? Add the missing ones.`,
38
+ `- Baseline: does every control have every [baseline] item that fits`,
39
+ ` it? Add the missing criteria to the right section.`,
40
+ `- Combinations: is every combination the source states written out?`,
41
+ `- Code branches: read the code this journey runs, client and server.`,
42
+ ` Is every validation, error response and conditional branch an`,
43
+ ` [observed] criterion or a Question? Add the missing ones.`,
44
+ `- Tiers: does every [spec] criterion quote a real source sentence?`,
45
+ ` Re-tag any that does not. Does any criterion contradict the code`,
46
+ ` with nothing to test (a rule the screen has no way to break)? Turn`,
47
+ ` it into a "Likely defect" note or a Question.`,
48
+ `- Shared rules: read the other docs in the same folder and make shared`,
49
+ ` cases (password, redirects, payloads, formats) identical.`,
50
+ `- Coverage: rebuild the "## Coverage" table; no blank cell may remain.`,
51
+ ``,
52
+ `Rules: edit "${docPath}" in place and touch no other file. Keep every`,
53
+ `sound criterion. Keep the header, the section numbering style and the`,
54
+ `last sections (Coverage, Test setup, Questions for product). Never`,
55
+ `write "# TODO". When done, print ONE line: what you added or fixed,`,
56
+ `with counts.`,
57
+ ].join('\n');
58
+ }
59
+ /**
60
+ * Review each doc (paths relative to docsDir) in its own AI pass, a few at
61
+ * a time. A failed review leaves that doc as drafted and is reported, never
62
+ * fatal: the draft is still a valid doc.
63
+ */
64
+ export async function reviewDocs(docsDir, docs, srcDir, reference) {
65
+ if (!docs.length)
66
+ return;
67
+ const config = loadConfig();
68
+ const base = splitCommand(config.aiCommand);
69
+ const { program, args } = reference ? withReferenceTools(base) : withEditTools(base);
70
+ console.log(`\nReviewing ${docs.length} journey doc(s) for missing scenarios...`);
71
+ await pool(docs, Math.max(1, config.aiConcurrency ?? 4), async (doc) => {
72
+ const path = join(docsDir, doc);
73
+ try {
74
+ const out = await runWithStdin(program, args, buildReviewPrompt(path, srcDir, reference), config.aiTimeoutMs);
75
+ console.log(` ${doc}: ${out.trim().split('\n').pop() ?? 'reviewed'}`);
76
+ }
77
+ catch (err) {
78
+ console.log(` ${doc}: review failed, kept as drafted (${err.message.split('\n')[0]})`);
79
+ }
80
+ });
81
+ }
@@ -18,7 +18,7 @@ const MAP_PATH = 'testforge.md';
18
18
  * Spawn a command, feed `input` on stdin, resolve with its stdout.
19
19
  * Async (unlike execFileSync) so many calls can run concurrently.
20
20
  */
21
- function runWithStdin(program, args, input, timeoutMs = 300_000) {
21
+ export function runWithStdin(program, args, input, timeoutMs = 300_000) {
22
22
  return new Promise((resolve, reject) => {
23
23
  const child = spawn(program, args, {
24
24
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -45,7 +45,7 @@ const GEN_CONCURRENCY = 4;
45
45
  * the results. Keeps a heavy fan-out (15 AI calls) from spawning 15
46
46
  * processes at once while still finishing in a few waves, not serially.
47
47
  */
48
- async function pool(items, limit, fn) {
48
+ export async function pool(items, limit, fn) {
49
49
  const results = new Array(items.length);
50
50
  let next = 0;
51
51
  async function worker() {
@@ -40,9 +40,9 @@ function buildPlanPrompt(journey, map) {
40
40
  ` refunds) may need many. Do not pad, do not over-split, do not force`,
41
41
  ` a fixed number.`,
42
42
  `- Each file covers ONE concern and must be independently meaningful.`,
43
- `- "## Test setup" is what every file needs to run, not a behaviour, and`,
44
- ` "## Questions for product (not asserted)" is not tested: plan no file`,
45
- ` for either.`,
43
+ `- "## Controls" and "## Coverage" are indexes, "## Test setup" is what`,
44
+ ` every file needs to run, and "## Questions for product (not`,
45
+ ` asserted)" is not tested: plan no file for any of them.`,
46
46
  ``,
47
47
  `Output ONLY a JSON array (no prose, no markdown fences) of objects:`,
48
48
  ` [{ "slug": "<short-kebab-name>", "title": "<describe title>",`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@activate-core/test-forge",
3
- "version": "0.19.3",
3
+ "version": "1.0.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": {