@activate-core/test-forge 0.13.0 → 0.14.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.
- package/dist/core/ai-generator.js +73 -25
- package/dist/core/doc-guidance.js +6 -0
- package/dist/core/harness.js +8 -51
- package/dist/core/reference.js +10 -0
- package/dist/core/report.js +1 -1
- package/dist/core/run-generate.js +12 -21
- package/dist/core/suite-planner.js +11 -3
- package/package.json +1 -1
- package/reporters/_shared.cjs +64 -4
- package/reporters/jest-reporter.cjs +5 -3
- package/reporters/playwright-reporter.cjs +4 -3
|
@@ -83,21 +83,9 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath, transpor
|
|
|
83
83
|
: 'TypeScript';
|
|
84
84
|
return [
|
|
85
85
|
`You are a senior QA engineer. Generate a complete ${framework}`,
|
|
86
|
-
`for the user journey described below
|
|
86
|
+
`for the user journey described below. (The specific file to write is`,
|
|
87
|
+
`stated at the very end, after the journey — read all the context first.)`,
|
|
87
88
|
``,
|
|
88
|
-
...(focus
|
|
89
|
-
? [
|
|
90
|
-
`SCOPE — this is ONE file in a larger suite for this journey.`,
|
|
91
|
-
`Cover ONLY this concern in this file:`,
|
|
92
|
-
` ${focus.title}`,
|
|
93
|
-
` ${focus.covers}`,
|
|
94
|
-
`Do NOT test the journey's other concerns here — separate files`,
|
|
95
|
-
`cover those. Go DEEP on this one concern (happy path + its own`,
|
|
96
|
-
`edge/negative/security cases) rather than broad-but-shallow.`,
|
|
97
|
-
`Use this exact describe() title: "${focus.title}".`,
|
|
98
|
-
``,
|
|
99
|
-
]
|
|
100
|
-
: []),
|
|
101
89
|
`Think like an experienced tester whose goal is to break the`,
|
|
102
90
|
`application. Cover, where relevant: happy path, missing/empty`,
|
|
103
91
|
`required fields, invalid formats and boundary values (very long,`,
|
|
@@ -117,6 +105,47 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath, transpor
|
|
|
117
105
|
`guard is missing and produce a failing test that falsely brands the`,
|
|
118
106
|
`product as vulnerable.`,
|
|
119
107
|
``,
|
|
108
|
+
`PRIME DIRECTIVE — A FAILING TEST MUST MEAN A REAL PRODUCT DEFECT.`,
|
|
109
|
+
`Teams hold releases on what these tests report. A test that fails for`,
|
|
110
|
+
`any reason other than the product being wrong is worse than no test at`,
|
|
111
|
+
`all: it burns the team's trust, and the next real defect gets waved`,
|
|
112
|
+
`through as "probably the tool again". So a test may FAIL only when the`,
|
|
113
|
+
`product provably contradicts an expectation the journey document`,
|
|
114
|
+
`states EXPLICITLY. Apply these rules without exception:`,
|
|
115
|
+
`(1) EXPLICIT-ORACLE ONLY. Before writing an assertion, find the exact`,
|
|
116
|
+
` line in the journey document that fixes the expected outcome, and`,
|
|
117
|
+
` cite it in a short comment (e.g. "// oracle: §4 total = fee +`,
|
|
118
|
+
` shipping"). If the expectation is merely implied, inferred, or`,
|
|
119
|
+
` "good practice" — including a rule that sounds obviously right —`,
|
|
120
|
+
` it is NOT an oracle: write it ${pending} with a "// TODO:" note.`,
|
|
121
|
+
` An assertion you cannot trace to a stated requirement is a false`,
|
|
122
|
+
` positive waiting to happen. Anything under a "Suggested (not a`,
|
|
123
|
+
` requirement…)" heading is explicitly NOT an oracle — never assert`,
|
|
124
|
+
` it; make it ${pending} if you cover it at all.`,
|
|
125
|
+
`(2) INFRASTRUCTURE IS NEVER A PRODUCT DEFECT. If a precondition cannot`,
|
|
126
|
+
` be established — a dependency is unreachable, a required env var or`,
|
|
127
|
+
` credential is absent, seed/fixture data is missing, a second`,
|
|
128
|
+
` identity is unavailable, an external provider is down — SKIP the`,
|
|
129
|
+
` test with a one-line reason naming exactly what was missing. Never`,
|
|
130
|
+
` let a missing precondition reach an assertion and report as a bug.`,
|
|
131
|
+
` Guard such tests at the top: check the precondition, skip if unmet.`,
|
|
132
|
+
`(3) NO FLAKY TESTS. Every test must pass or fail for the same reason on`,
|
|
133
|
+
` every run, in any order. Do not depend on execution order, on data`,
|
|
134
|
+
` another test creates or mutates, on wall-clock timing, or on`,
|
|
135
|
+
` ambient totals that other tests change (assert on the rows YOU`,
|
|
136
|
+
` seeded, by id — never on a global count). Set up what you assert`,
|
|
137
|
+
` on, and assert only on that. Wait for a specific condition, never a`,
|
|
138
|
+
` fixed sleep.`,
|
|
139
|
+
`(4) NEVER ASSERT AN UNFIXED VALUE. Do not assert exact user-facing copy,`,
|
|
140
|
+
` incidental ordering, generated ids, timestamps, or counts the`,
|
|
141
|
+
` document does not fix. Assert the behaviour the document specifies,`,
|
|
142
|
+
` at the loosest form that still proves it.`,
|
|
143
|
+
`(5) A TIMEOUT IS NOT A BUG REPORT. If an action legitimately waits on`,
|
|
144
|
+
` something external, verify availability first and skip when absent.`,
|
|
145
|
+
` A test that times out tells the team nothing about the product.`,
|
|
146
|
+
`When rules conflict, prefer reporting LESS: a pending test costs a line`,
|
|
147
|
+
`in the report; a false failure costs the team a release.`,
|
|
148
|
+
``,
|
|
120
149
|
`GROUND EVERYTHING IN THE REAL SOURCE. You are running inside the`,
|
|
121
150
|
`project's repository and have read-only tools (Read, Grep, Glob, LS).`,
|
|
122
151
|
`BEFORE writing tests, look up the actual code for this journey — use`,
|
|
@@ -241,17 +270,6 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath, transpor
|
|
|
241
270
|
`module (aliased as apiRequest above).`,
|
|
242
271
|
].join('\n'),
|
|
243
272
|
``,
|
|
244
|
-
...(testPath
|
|
245
|
-
? [
|
|
246
|
-
`This test file will be saved at EXACTLY this path:`,
|
|
247
|
-
` ${testPath}`,
|
|
248
|
-
`Compute every require()/import path (to app source, helpers, the`,
|
|
249
|
-
`seed file, etc.) RELATIVE TO THAT EXACT location — count the`,
|
|
250
|
-
`directory depth from that path, do not assume a conventional`,
|
|
251
|
-
`depth. Getting the relative depth wrong breaks the whole file.`,
|
|
252
|
-
``,
|
|
253
|
-
]
|
|
254
|
-
: []),
|
|
255
273
|
...(transport
|
|
256
274
|
? [transport, `Do NOT import any library that is not already a dependency of this project.`, ``]
|
|
257
275
|
: []),
|
|
@@ -298,6 +316,36 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath, transpor
|
|
|
298
316
|
`Data section. Use ALL of it. Respect any "# TODO:" notes.`,
|
|
299
317
|
``,
|
|
300
318
|
journey.rawMarkdown,
|
|
319
|
+
// ---- PER-FILE TASK (kept LAST so the big context above stays a stable,
|
|
320
|
+
// cache-reusable prefix across every file in this journey, and so the
|
|
321
|
+
// concrete task is the most recent thing the model reads) ----
|
|
322
|
+
``,
|
|
323
|
+
...(focus
|
|
324
|
+
? [
|
|
325
|
+
`--- THIS FILE (the one to write now) ---`,
|
|
326
|
+
`This is ONE file in a larger suite for this journey.`,
|
|
327
|
+
`Cover ONLY this concern here:`,
|
|
328
|
+
` ${focus.title}`,
|
|
329
|
+
` ${focus.covers}`,
|
|
330
|
+
`Do NOT test the journey's other concerns here — separate files`,
|
|
331
|
+
`cover those. Go DEEP on this one concern (happy path + its own`,
|
|
332
|
+
`edge/negative/security cases) rather than broad-but-shallow.`,
|
|
333
|
+
`Use this exact describe() title: "${focus.title}".`,
|
|
334
|
+
]
|
|
335
|
+
: []),
|
|
336
|
+
...(testPath
|
|
337
|
+
? [
|
|
338
|
+
``,
|
|
339
|
+
`Save this file at EXACTLY: ${testPath}`,
|
|
340
|
+
`Compute every require()/import path (app source, helpers, the seed`,
|
|
341
|
+
`file, etc.) RELATIVE TO THAT EXACT location — count the directory`,
|
|
342
|
+
`depth from that path; do not assume a conventional depth. Getting`,
|
|
343
|
+
`the relative depth wrong breaks the whole file.`,
|
|
344
|
+
]
|
|
345
|
+
: []),
|
|
346
|
+
``,
|
|
347
|
+
`Now output ONLY the raw ${isJs ? 'JavaScript' : 'TypeScript'} test code`,
|
|
348
|
+
`for THIS file — the first character must be the first line of code.`,
|
|
301
349
|
].join('\n');
|
|
302
350
|
}
|
|
303
351
|
/**
|
|
@@ -20,6 +20,12 @@ export const DEEP_DOC_GUIDANCE = [
|
|
|
20
20
|
` vague mentions;`,
|
|
21
21
|
`- concrete acceptance criteria per case (exactly what must hold).`,
|
|
22
22
|
`Aim for the depth of a spec a new engineer could implement from.`,
|
|
23
|
+
`Every acceptance criterion you write becomes an ORACLE — tests are`,
|
|
24
|
+
`generated from it, and a product that disagrees is reported to the team`,
|
|
25
|
+
`as a defect. So state a criterion ONLY where the source actually fixes`,
|
|
26
|
+
`the behaviour. Anything you merely believe SHOULD hold goes under a`,
|
|
27
|
+
`"Suggested (not a requirement — do not assert)" heading, never into the`,
|
|
28
|
+
`acceptance criteria: an invented requirement becomes a false bug report.`,
|
|
23
29
|
`Organise the body with numbered "## N. <Section>" headings, ONE`,
|
|
24
30
|
`concern per section, because each section later becomes its own`,
|
|
25
31
|
`focused test file — so the richer and better-separated the sections,`,
|
package/dist/core/harness.js
CHANGED
|
@@ -285,10 +285,10 @@ export function playwrightConfig(testDirRelToPackage, globalSetupRelToPackage, c
|
|
|
285
285
|
` testMatch: /.*\\.(test|spec)\\.[jt]s$/,`,
|
|
286
286
|
` globalSetup: require.resolve('${globalSetupRelToPackage}'),`,
|
|
287
287
|
` timeout: 60000,`,
|
|
288
|
-
` // Console line output
|
|
289
|
-
` //
|
|
290
|
-
` //
|
|
291
|
-
` reporter: [['line'], [
|
|
288
|
+
` // Console line output. The last reporter auto-publishes the run to the`,
|
|
289
|
+
` // TestForge dashboard (needs TESTFORGE_KEY in env + project/dashboardUrl`,
|
|
290
|
+
` // in testforge.config.json).`,
|
|
291
|
+
` reporter: [['line'], [require.resolve('@activate-core/test-forge/reporters/playwright-reporter.cjs'), { category: ${JSON.stringify(category)} }]],`,
|
|
292
292
|
` use: {`,
|
|
293
293
|
` baseURL: process.env.BASE_URL || 'http://localhost:3000',`,
|
|
294
294
|
` trace: 'on-first-retry',`,
|
|
@@ -300,8 +300,8 @@ export function playwrightConfig(testDirRelToPackage, globalSetupRelToPackage, c
|
|
|
300
300
|
}
|
|
301
301
|
/**
|
|
302
302
|
* Jest config for a backend (jest) area — self-contained so `npm test` / a bare
|
|
303
|
-
* `npx jest` runs with no shell setup
|
|
304
|
-
* - testEnvironment '
|
|
303
|
+
* `npx jest` runs with no shell setup.
|
|
304
|
+
* - testEnvironment 'node': the default Node test environment
|
|
305
305
|
* - setupFiles: loads .env.testing + NODE_ENV + the reset opt-in before any
|
|
306
306
|
* test file imports the app
|
|
307
307
|
* - serial (maxWorkers 1): suites share one test DB and reset between tests
|
|
@@ -309,9 +309,9 @@ export function playwrightConfig(testDirRelToPackage, globalSetupRelToPackage, c
|
|
|
309
309
|
export function jestConfig(testDirRelToPackage, setupRelToPackage, category = 'api') {
|
|
310
310
|
return [
|
|
311
311
|
`// @generated by testforge`,
|
|
312
|
-
`// Install once: npm i -D jest
|
|
312
|
+
`// Install once: npm i -D jest`,
|
|
313
313
|
`module.exports = {`,
|
|
314
|
-
` testEnvironment: '
|
|
314
|
+
` testEnvironment: 'node',`,
|
|
315
315
|
` setupFiles: ['<rootDir>/${setupRelToPackage}'],`,
|
|
316
316
|
` testMatch: ['<rootDir>/${testDirRelToPackage}/**/*.test.js'],`,
|
|
317
317
|
` // Serial: the suites share ONE test DB and reset it between tests.`,
|
|
@@ -350,49 +350,6 @@ export function jestSetupContent() {
|
|
|
350
350
|
``,
|
|
351
351
|
].join('\n');
|
|
352
352
|
}
|
|
353
|
-
/** Allure failure categories — buckets that show up in the report's "Categories"
|
|
354
|
-
* tab. Copied into allure-results/ so the report groups failures meaningfully. */
|
|
355
|
-
export function allureCategories() {
|
|
356
|
-
return JSON.stringify([
|
|
357
|
-
{ name: 'Product defect', matchedStatuses: ['failed'], messageRegex: '.*expect.*|.*assert.*' },
|
|
358
|
-
{ name: 'Test / harness issue', matchedStatuses: ['broken'] },
|
|
359
|
-
{ name: 'Flaky', matchedStatuses: ['failed', 'broken'], flaky: true },
|
|
360
|
-
], null, 2);
|
|
361
|
-
}
|
|
362
|
-
/** ALLURE.md — how to install the reporter deps and view the report. */
|
|
363
|
-
export function allureDoc() {
|
|
364
|
-
return [
|
|
365
|
-
`# Allure reporting (generated by testforge)`,
|
|
366
|
-
``,
|
|
367
|
-
`testforge wires the suites to emit **Allure** results. To view the report:`,
|
|
368
|
-
``,
|
|
369
|
-
`## 1. Install once (per package)`,
|
|
370
|
-
'```bash',
|
|
371
|
-
`# web (Playwright): npm i -D allure-playwright allure-commandline`,
|
|
372
|
-
`# api (Jest): npm i -D allure-jest allure-commandline`,
|
|
373
|
-
'```',
|
|
374
|
-
``,
|
|
375
|
-
`## 2. Run the tests`,
|
|
376
|
-
`Running the suite writes raw results into \`allure-results/\`.`,
|
|
377
|
-
``,
|
|
378
|
-
`## 3. Generate + open the report`,
|
|
379
|
-
'```bash',
|
|
380
|
-
`npx allure generate allure-results --clean -o allure-report`,
|
|
381
|
-
`npx allure open allure-report`,
|
|
382
|
-
'```',
|
|
383
|
-
`Or add scripts to package.json:`,
|
|
384
|
-
'```json',
|
|
385
|
-
`"report": "allure generate allure-results --clean -o allure-report && allure open allure-report"`,
|
|
386
|
-
'```',
|
|
387
|
-
``,
|
|
388
|
-
`## Categories`,
|
|
389
|
-
`\`allure-categories.json\` groups failures (Product defect / Test-harness / Flaky).`,
|
|
390
|
-
`Copy it to \`allure-results/categories.json\` before generating to activate it.`,
|
|
391
|
-
``,
|
|
392
|
-
`> \`allure-results/\`, \`allure-report/\` are build output — add them to .gitignore.`,
|
|
393
|
-
``,
|
|
394
|
-
].join('\n');
|
|
395
|
-
}
|
|
396
353
|
/**
|
|
397
354
|
* Playwright globalSetup file: runs the testkit's one-call setup() once
|
|
398
355
|
* before the suite (connect + seed + mint/populate tokens). Sits next to
|
package/dist/core/reference.js
CHANGED
|
@@ -147,6 +147,16 @@ export function buildReferenceDiscoverPrompt(srcDir, docsDir, reference, journey
|
|
|
147
147
|
`Rules:`,
|
|
148
148
|
`- Base behavior on the reference and technical detail on real code;`,
|
|
149
149
|
` do not invent either with false confidence.`,
|
|
150
|
+
`- EVERY requirement you write becomes an ORACLE: tests will be`,
|
|
151
|
+
` generated from it, and a product that disagrees will be reported to`,
|
|
152
|
+
` the team as a defect. So write a requirement ONLY when the reference`,
|
|
153
|
+
` actually states it. Do NOT add hardening, validation, limits, or`,
|
|
154
|
+
` guards that the reference does not ask for, however sensible they`,
|
|
155
|
+
` seem — an invented requirement becomes a false bug report and costs`,
|
|
156
|
+
` the team a release.`,
|
|
157
|
+
`- When you believe something SHOULD hold but the reference does not say`,
|
|
158
|
+
` so, put it under a "Suggested (not a requirement — do not assert)"`,
|
|
159
|
+
` heading, never in Expected / Negative Cases / acceptance criteria.`,
|
|
150
160
|
`- Where the reference is silent on a needed detail, add a "# TODO:"`,
|
|
151
161
|
` line describing what the developer must verify.`,
|
|
152
162
|
`- Never include secrets — refer to env var names only.`,
|
package/dist/core/report.js
CHANGED
|
@@ -145,7 +145,7 @@ for (const f of ['jest.config.js','jest.config.cjs','jest.config.json']) {
|
|
|
145
145
|
if (!Object.keys(base).length && fs.existsSync(path.join(__dirname,'package.json'))) base = require('./package.json').jest || {};
|
|
146
146
|
module.exports = {
|
|
147
147
|
...base,
|
|
148
|
-
testEnvironment: 'node', //
|
|
148
|
+
testEnvironment: 'node', // plain node env for a count-only run
|
|
149
149
|
setupFilesAfterEnv: [...(base.setupFilesAfterEnv || []), path.resolve(__dirname, '.tf-collect-patch.cjs')],
|
|
150
150
|
};
|
|
151
151
|
`;
|
|
@@ -27,7 +27,7 @@ import { generateSeed } from './seed-generator.js';
|
|
|
27
27
|
import { planSuite } from './suite-planner.js';
|
|
28
28
|
import { isSyntaxValid, checkTestGates, analyzeFile } from './validate.js';
|
|
29
29
|
import { installedModules, transportHint, projectSelectors } from './detect.js';
|
|
30
|
-
import { harnessInfo, generateHarnessAsync, generateHarness, playwrightConfig, globalSetupContent, fixturesDoc, jestConfig, jestSetupContent,
|
|
30
|
+
import { harnessInfo, generateHarnessAsync, generateHarness, playwrightConfig, globalSetupContent, fixturesDoc, jestConfig, jestSetupContent, } from './harness.js';
|
|
31
31
|
import { hashContent, loadCache, saveCache } from './cache.js';
|
|
32
32
|
import { findMarkdownFiles } from './find-files.js';
|
|
33
33
|
import { loadConfig, testDirForType, resolveFramework } from './config.js';
|
|
@@ -356,7 +356,7 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
|
|
|
356
356
|
writeFileSync(info.path, kit, 'utf-8');
|
|
357
357
|
writeFileSync(join(info.dir, 'FIXTURES.md'), fixturesDoc(), 'utf-8');
|
|
358
358
|
// Config + globalSetup (playwright) / config + setup (jest) so the specs run
|
|
359
|
-
// AND get their tokens minted once, plus
|
|
359
|
+
// AND get their tokens minted once, plus the runner config scaffolding.
|
|
360
360
|
writeHarnessConfigs(framework, areaDir, info.dir, area);
|
|
361
361
|
harnessByArea.set(area, info.path);
|
|
362
362
|
return info.path;
|
|
@@ -376,16 +376,15 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
|
|
|
376
376
|
// --journey <x>: generate just the one matching journey.
|
|
377
377
|
if (only && !matchesJourney(file, journey.id, only))
|
|
378
378
|
continue;
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
// (testDir/<journey>-<slug>). Otherwise they'd silently not run.
|
|
379
|
+
// EVERY area gets its own per-journey FOLDER of focused files
|
|
380
|
+
// (testDir/<journey>/<slug>), for both Playwright and Jest. The folder
|
|
381
|
+
// namespaces the files, so the on-disk name is just the clean <slug>
|
|
382
|
+
// (no journey prefix) — which is also what the dashboard shows as the
|
|
383
|
+
// suite name. testforge's generated jest.config uses a recursive
|
|
384
|
+
// `tests/**/*.test.js` glob, so foldered jest files are still discovered.
|
|
386
385
|
const areaDir = testDirForType(config, journey.area);
|
|
387
386
|
const framework = resolveFramework(config, journey.area, journey.type);
|
|
388
|
-
const flat =
|
|
387
|
+
const flat = false; // foldered layout for all runners (clean, categorised)
|
|
389
388
|
const testDir = flat ? areaDir : join(areaDir, journey.id);
|
|
390
389
|
const seedPath = join(seedDir, `${journey.id}${seedExt}`);
|
|
391
390
|
// Dependency-aware guardrails (F1): what this area may import, and how
|
|
@@ -631,23 +630,15 @@ function writeCoverageManifest(results) {
|
|
|
631
630
|
}
|
|
632
631
|
}
|
|
633
632
|
/**
|
|
634
|
-
* Write the per-area runner config
|
|
635
|
-
* - playwright: global-setup.js + playwright.config.js
|
|
636
|
-
* - jest: helpers/jest.setup.js + jest.config.js
|
|
637
|
-
* - both: ALLURE.md + allure-categories.json at the package root
|
|
633
|
+
* Write the per-area runner config.
|
|
634
|
+
* - playwright: global-setup.js + playwright.config.js
|
|
635
|
+
* - jest: helpers/jest.setup.js + jest.config.js
|
|
638
636
|
* Config files are written only if absent, so a project's hand-tuned config is
|
|
639
637
|
* never clobbered. `harnessDir` is the area's helpers dir (where setup lives).
|
|
640
638
|
*/
|
|
641
639
|
function writeHarnessConfigs(framework, areaDir, harnessDir, area) {
|
|
642
640
|
const pkgRoot = dirname(areaDir);
|
|
643
641
|
const rel = areaDir.slice(pkgRoot.length + 1) || '.';
|
|
644
|
-
// Allure docs + categories (both frameworks); never overwrite.
|
|
645
|
-
const allureMd = join(pkgRoot, 'ALLURE.md');
|
|
646
|
-
if (!existsSync(allureMd))
|
|
647
|
-
writeFileSync(allureMd, allureDoc(), 'utf-8');
|
|
648
|
-
const cats = join(pkgRoot, 'allure-categories.json');
|
|
649
|
-
if (!existsSync(cats))
|
|
650
|
-
writeFileSync(cats, allureCategories(), 'utf-8');
|
|
651
642
|
if (framework === 'playwright') {
|
|
652
643
|
const gsPath = join(harnessDir, 'global-setup.js');
|
|
653
644
|
writeFileSync(gsPath, globalSetupContent(), 'utf-8');
|
|
@@ -44,7 +44,13 @@ function buildPlanPrompt(journey, map) {
|
|
|
44
44
|
`Output ONLY a JSON array (no prose, no markdown fences) of objects:`,
|
|
45
45
|
` [{ "slug": "<short-kebab-name>", "title": "<describe title>",`,
|
|
46
46
|
` "covers": "<one or two sentences: exactly what this file tests>" }]`,
|
|
47
|
-
`slug: lowercase
|
|
47
|
+
`slug: lowercase kebab-case, no path, no extension — and SHORT: name the`,
|
|
48
|
+
` one concern in 2–4 words (4 max), the way a human names a file. Do NOT`,
|
|
49
|
+
` restate the journey name and do NOT pile on qualifiers.`,
|
|
50
|
+
` Good: "amount-validation", "3ds-challenge", "zero-amount", "rate-limit".`,
|
|
51
|
+
` Bad: "charge-amount-sanity-negative-nonnumeric-oversized",`,
|
|
52
|
+
` "otp-session-otp-send-rate-limit".`,
|
|
53
|
+
`title: human-readable and specific, e.g. "Payment — Amount validation".`,
|
|
48
54
|
`The very first character of your response must be "[".`,
|
|
49
55
|
``,
|
|
50
56
|
`--- PROJECT MAP (data layer) ---`,
|
|
@@ -71,14 +77,16 @@ function extractJsonArray(text) {
|
|
|
71
77
|
return [];
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
|
-
/** Normalise a raw slug into a safe kebab-case file stem. */
|
|
80
|
+
/** Normalise a raw slug into a safe, SHORT kebab-case file stem. */
|
|
75
81
|
function safeSlug(raw) {
|
|
76
|
-
|
|
82
|
+
const kebab = String(raw ?? '')
|
|
77
83
|
.trim()
|
|
78
84
|
.toLowerCase()
|
|
79
85
|
.replace(/\.(spec|test)\.[jt]sx?$/, '') // drop any extension the AI added
|
|
80
86
|
.replace(/[^a-z0-9]+/g, '-')
|
|
81
87
|
.replace(/^-|-$/g, '');
|
|
88
|
+
// Hard cap: keep names short & human — at most 4 words, whatever the AI returns.
|
|
89
|
+
return kebab.split('-').filter(Boolean).slice(0, 4).join('-');
|
|
82
90
|
}
|
|
83
91
|
/**
|
|
84
92
|
* Plan the suite for one journey: returns the list of focused files to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@activate-core/test-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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": {
|
package/reporters/_shared.cjs
CHANGED
|
@@ -42,6 +42,50 @@ function loadConfig(root) {
|
|
|
42
42
|
|
|
43
43
|
const strip = (s) => (s || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
44
44
|
|
|
45
|
+
// jest's raw matcher line (`expect(received).toBeGreaterThanOrEqual(expected)`) reads
|
|
46
|
+
// backwards to a human. Translate the matcher into a plain, direction-correct sentence
|
|
47
|
+
// so an engineer sees intent, not jest internals.
|
|
48
|
+
const MATCHER = {
|
|
49
|
+
toBe: 'equal', toEqual: 'equal', toStrictEqual: 'equal',
|
|
50
|
+
toBeGreaterThanOrEqual: 'be at least', toBeGreaterThan: 'be greater than',
|
|
51
|
+
toBeLessThanOrEqual: 'be at most', toBeLessThan: 'be less than',
|
|
52
|
+
toContain: 'contain', toContainEqual: 'contain', toMatch: 'match',
|
|
53
|
+
toHaveLength: 'have length', toHaveProperty: 'have property', toBeCloseTo: 'be about',
|
|
54
|
+
};
|
|
55
|
+
function humanize(body) {
|
|
56
|
+
const m = body.match(/\.(\w+)\((?:expected|n|value|substring|sample|element)?\)/) || body.match(/expect\([^)]*\)\.(\w+)/);
|
|
57
|
+
const phrase = m && MATCHER[m[1]];
|
|
58
|
+
if (!phrase) return null;
|
|
59
|
+
const exp = (body.match(/Expected:?\s*(.+)/) || [])[1];
|
|
60
|
+
const rec = (body.match(/Received:?\s*(.+)/) || [])[1];
|
|
61
|
+
const ce = (exp || '').replace(/^[><=!~\s]+/, '').trim();
|
|
62
|
+
const cr = (rec || '').trim();
|
|
63
|
+
if (!ce || !cr) return null;
|
|
64
|
+
return `The check expected the result to ${phrase} ${ce}, but it was ${cr}.`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A readable failure detail for an engineer: a plain-language lead (what the check
|
|
68
|
+
// wanted vs what it got) + the Expected/Received values + any custom message. Drops
|
|
69
|
+
// jest's cryptic matcher line and the noisy stack frames / internal paths. Capped so
|
|
70
|
+
// it stays a summary, not a full dump.
|
|
71
|
+
function cleanError(raw) {
|
|
72
|
+
const s = strip(raw || '');
|
|
73
|
+
if (!s) return '';
|
|
74
|
+
const kept = s.split('\n').filter((l) => {
|
|
75
|
+
const t = l.trim();
|
|
76
|
+
if (!t) return true; // keep blank lines for readability (collapsed below)
|
|
77
|
+
if (/^at\s/.test(t)) return false; // stack frame
|
|
78
|
+
if (/node_modules|node:internal|\/dist\//.test(t)) return false; // library/internal noise
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
const body = kept.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
82
|
+
const lead = humanize(body);
|
|
83
|
+
if (!lead) return body.slice(0, 700);
|
|
84
|
+
// The plain lead replaces jest's `expect(received)…` matcher line — drop it, keep the rest.
|
|
85
|
+
const rest = body.split('\n').filter((l) => !/^(Error:\s*)?expect\(/.test(l.trim())).join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
86
|
+
return (lead + (rest ? '\n\n' + rest : '')).slice(0, 700);
|
|
87
|
+
}
|
|
88
|
+
|
|
45
89
|
function sh(cmd, cwd) {
|
|
46
90
|
try { return (execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()) || undefined; }
|
|
47
91
|
catch { return undefined; }
|
|
@@ -73,11 +117,27 @@ function resolveEnv() {
|
|
|
73
117
|
|
|
74
118
|
function sig(msg) {
|
|
75
119
|
const m = strip(msg || '').replace(/\n/g, ' ');
|
|
76
|
-
|
|
120
|
+
// Order matters, and each pattern has to be precise. A failing assertion
|
|
121
|
+
// carries plenty of incidental words — Playwright prints "Timeout: 5000ms"
|
|
122
|
+
// inside every expect() failure, and a status-code assertion legitimately
|
|
123
|
+
// mentions 403 — so loose patterns used to swallow most failures into
|
|
124
|
+
// "Timeout" / "IM auth" and made the whole triage useless.
|
|
125
|
+
|
|
126
|
+
// 1) A REAL timeout: the runner itself gave up, or the socket did.
|
|
127
|
+
if (/test timeout of \d+ ?ms exceeded|exceeded timeout of \d+ ?ms|timed out after \d+|ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED/i.test(m))
|
|
128
|
+
return 'Timeout';
|
|
129
|
+
|
|
130
|
+
// 2) An assertion that ran and disagreed — the common, useful case.
|
|
131
|
+
if (/expect\(.*\)|toBe\b|toEqual\b|toHaveCount\b|Received:|Expected:|expected the result to/i.test(m))
|
|
132
|
+
return 'Assertion mismatch';
|
|
133
|
+
|
|
134
|
+
// 3) Auth against the identity service — needs auth wording, not a bare 403.
|
|
135
|
+
if (/whoami|invalid_grant|refresh token|Unauthorized|Forbidden|invalid token|jwt (expired|malformed)/i.test(m))
|
|
136
|
+
return 'IM auth (403)';
|
|
137
|
+
|
|
77
138
|
if (/\$gt|\$exists|injection/i.test(m)) return 'NoSQL injection accepted';
|
|
78
|
-
if (/timeout|timed out|ETIMEDOUT/i.test(m)) return 'Timeout';
|
|
79
139
|
if (/\b500\b|Internal Server/i.test(m)) return 'Server 500';
|
|
80
|
-
if (/
|
|
140
|
+
if (/timeout|timed out/i.test(m)) return 'Timeout'; // looser fallback, last
|
|
81
141
|
return 'Uncategorised';
|
|
82
142
|
}
|
|
83
143
|
|
|
@@ -106,4 +166,4 @@ async function publish(category, suites, durationSec) {
|
|
|
106
166
|
}
|
|
107
167
|
}
|
|
108
168
|
|
|
109
|
-
module.exports = { strip, sig, publish, gitInfo, resolveEnv };
|
|
169
|
+
module.exports = { strip, cleanError, sig, publish, gitInfo, resolveEnv };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// TestForge Jest reporter — auto-publishes the run to the dashboard on completion.
|
|
2
2
|
// Add to jest.config.js:
|
|
3
3
|
// reporters: ['default', ['<abs>/reporters/jest-reporter.cjs', { category: 'api' }]]
|
|
4
|
-
const {
|
|
4
|
+
const { cleanError, sig, publish } = require('./_shared.cjs');
|
|
5
5
|
|
|
6
6
|
module.exports = class TestForgeJestReporter {
|
|
7
7
|
constructor(_globalConfig, options) {
|
|
@@ -15,12 +15,14 @@ module.exports = class TestForgeJestReporter {
|
|
|
15
15
|
for (const a of (f.testResults || f.assertionResults || [])) {
|
|
16
16
|
const status = a.status === 'passed' ? 'passed' : a.status === 'failed' ? 'failed' : 'skipped';
|
|
17
17
|
const raw = (a.failureMessages || [])[0];
|
|
18
|
-
const err = strip(raw || '').split('\n').find((l) => l && l.trim()) || '';
|
|
19
18
|
su.tests.push({
|
|
20
19
|
name: a.fullName || a.title,
|
|
21
20
|
status,
|
|
22
|
-
error: status === 'failed' ?
|
|
21
|
+
error: status === 'failed' ? cleanError(raw) : undefined,
|
|
23
22
|
sig: status === 'failed' ? sig(raw) : undefined,
|
|
23
|
+
note: status === 'skipped'
|
|
24
|
+
? (a.status === 'todo' ? 'Marked to-do — this scenario isn’t implemented yet' : 'Skipped — not run in this execution')
|
|
25
|
+
: undefined,
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// TestForge Playwright reporter — auto-publishes the run to the dashboard on completion.
|
|
2
2
|
// Add to playwright.config.js reporter array:
|
|
3
3
|
// ['<abs>/reporters/playwright-reporter.cjs', { category: 'web' }]
|
|
4
|
-
const {
|
|
4
|
+
const { cleanError, sig, publish } = require('./_shared.cjs');
|
|
5
5
|
|
|
6
6
|
module.exports = class TestForgePlaywrightReporter {
|
|
7
7
|
constructor(options) {
|
|
@@ -15,12 +15,13 @@ module.exports = class TestForgePlaywrightReporter {
|
|
|
15
15
|
const su = this.suites[name] || (this.suites[name] = { name, category: this.category, tests: [] });
|
|
16
16
|
const status = result.status === 'passed' ? 'passed' : result.status === 'skipped' ? 'skipped' : 'failed';
|
|
17
17
|
const raw = result.error && result.error.message;
|
|
18
|
-
const
|
|
18
|
+
const skipAnno = (test.annotations || []).find((x) => x.type === 'skip' || x.type === 'fixme');
|
|
19
19
|
su.tests.push({
|
|
20
20
|
name: test.title,
|
|
21
21
|
status,
|
|
22
|
-
error: status === 'failed' ?
|
|
22
|
+
error: status === 'failed' ? cleanError(raw) : undefined,
|
|
23
23
|
sig: status === 'failed' ? sig(raw) : undefined,
|
|
24
|
+
note: status === 'skipped' ? ((skipAnno && skipAnno.description) || 'Skipped — not run in this execution') : undefined,
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
async onEnd(result) {
|