@activate-core/test-forge 0.7.0 → 0.8.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/cli/index.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * testforge generate --force Regenerate everything, ignoring the cache.
16
16
  */
17
17
  import { VERSION } from '../core/index.js';
18
- import { runGenerate } from '../core/run-generate.js';
18
+ import { runGenerate, runHarness } from '../core/run-generate.js';
19
19
  import { runDiscover } from '../core/discover.js';
20
20
  import { runMap } from '../core/map.js';
21
21
  import { runInit } from '../core/init.js';
@@ -75,6 +75,7 @@ function printUsage() {
75
75
  console.log(' testforge init Inspect project and write config');
76
76
  console.log(' testforge update Re-check and update only what changed');
77
77
  console.log(' testforge env Write .env.testing.example from docs');
78
+ console.log(' testforge harness [--area <a>] (Re)generate the shared testkit only');
78
79
  }
79
80
  async function main() {
80
81
  if (command === 'update') {
@@ -107,6 +108,19 @@ async function main() {
107
108
  else if (command === 'env') {
108
109
  runEnvTemplate('docs', '.env.testing.example', '.', generatedScanDirs());
109
110
  }
111
+ else if (command === 'harness') {
112
+ console.log('(Re)generating the shared test harness (testkit)...\n');
113
+ const written = await runHarness(flagValue('--area'));
114
+ if (written.length === 0) {
115
+ console.log('No harness generated. Check the --area or run `generate` first.');
116
+ }
117
+ else {
118
+ for (const p of written)
119
+ console.log(` testkit: ${p}`);
120
+ console.log('\nDone. Existing tests import this testkit unchanged; its setup()' +
121
+ ' seeds + mints/populates the auth tokens they read.');
122
+ }
123
+ }
110
124
  else if (command === 'generate') {
111
125
  const useAI = !useBasic;
112
126
  console.log(useAI
@@ -34,11 +34,37 @@ export const HARNESS_CONTRACT = [
34
34
  ` /collections + enums), returning the`,
35
35
  ` created ids/handles`,
36
36
  ` - TEST_IDS: object the stable well-known test ids/handles`,
37
+ ` - setup(): Promise<void> THE one-call entry point (idempotent,`,
38
+ ` safe to call from every beforeAll AND`,
39
+ ` from a Playwright globalSetup). It:`,
40
+ ` 1. connect()s + seed()s;`,
41
+ ` 2. MINTS a fresh auth access token at`,
42
+ ` runtime — read the project's REAL`,
43
+ ` login/refresh auth (controllers/`,
44
+ ` middleware) and either exchange a`,
45
+ ` seed token (env, e.g. a refresh`,
46
+ ` token) or log in the seeded qa-`,
47
+ ` user to obtain it;`,
48
+ ` 3. CACHES the minted token to a temp`,
49
+ ` file so parallel workers reuse it`,
50
+ ` (one mint per run, no churn);`,
51
+ ` 4. POPULATES process.env for EVERY`,
52
+ ` token/id env var the suite reads`,
53
+ ` — GREP the sibling test files for`,
54
+ ` process.env.*TOKEN*/*ID* names and`,
55
+ ` set each to the minted/seeded`,
56
+ ` value, so tests that read`,
57
+ ` process.env work with NO hand-`,
58
+ ` pasted tokens.`,
59
+ ` - token(role?): string the current access token accessor`,
37
60
  `Plus, for Playwright suites, ALSO export:`,
38
61
  ` - apiContext(request): a helper that returns an authed API request`,
39
- ` context (Bearer token from an env var — NEVER hardcode a secret)`,
62
+ ` context (Bearer = the minted token — NEVER hardcode a secret)`,
40
63
  ` - login(page): Promise<void> drive/seed an authenticated UI session`,
41
64
  `Use only env var NAMES for secrets/connection — never literal values.`,
65
+ `The ONLY credentials a developer should have to provide are the DB`,
66
+ `connection, a Stripe test key, and ONE seed/refresh token (or seeded`,
67
+ `login) — everything else setup() derives at runtime.`,
42
68
  ].join('\n');
43
69
  /** Where an area's shared testkit lives, and how tests import it. */
44
70
  export function harnessInfo(areaDir, framework, ext) {
@@ -64,8 +90,12 @@ function buildHarnessPrompt(harnessPath, framework) {
64
90
  `the project's REAL data layer and auth.`,
65
91
  ``,
66
92
  `You are inside the repository with read-only tools (Read, Grep, Glob,`,
67
- `LS). READ the real DB config, the models/schemas, and the auth`,
68
- `middleware, then implement the testkit against them.`,
93
+ `LS). READ the real DB config, the models/schemas, AND the real auth`,
94
+ `flow (login / refresh-token / OTP controllers + middleware) so setup()`,
95
+ `mints tokens the way the app actually does. Also GREP the sibling test`,
96
+ `files next to this testkit for every process.env.<NAME> they read`,
97
+ `(especially *TOKEN*, *_ID, base URLs) and make setup() POPULATE those`,
98
+ `exact names — the tests must run without any hand-pasted token.`,
69
99
  ``,
70
100
  `This is a ${framework === 'jest' ? 'Jest/backend' : 'Playwright/UI'} area.`,
71
101
  HARNESS_CONTRACT,
@@ -112,9 +142,10 @@ export function generateHarnessAsync(harnessPath, framework) {
112
142
  /**
113
143
  * A minimal Playwright config so a webapp/ops area that currently uses
114
144
  * Cypress can actually run the generated specs. Points at the area's test
115
- * dir and treats *.test.js as specs.
145
+ * dir, treats *.test.js as specs, and runs the testkit's setup() once via
146
+ * globalSetup (mints/populates tokens before any worker starts).
116
147
  */
117
- export function playwrightConfig(testDirRelToPackage) {
148
+ export function playwrightConfig(testDirRelToPackage, globalSetupRelToPackage) {
118
149
  return [
119
150
  `// @generated by testforge`,
120
151
  `// Minimal Playwright config so testforge's generated specs run.`,
@@ -124,6 +155,7 @@ export function playwrightConfig(testDirRelToPackage) {
124
155
  `module.exports = defineConfig({`,
125
156
  ` testDir: '${testDirRelToPackage}',`,
126
157
  ` testMatch: /.*\\.(test|spec)\\.[jt]s$/,`,
158
+ ` globalSetup: require.resolve('${globalSetupRelToPackage}'),`,
127
159
  ` timeout: 60000,`,
128
160
  ` use: {`,
129
161
  ` baseURL: process.env.BASE_URL || 'http://localhost:3000',`,
@@ -133,3 +165,21 @@ export function playwrightConfig(testDirRelToPackage) {
133
165
  ``,
134
166
  ].join('\n');
135
167
  }
168
+ /**
169
+ * Playwright globalSetup file: runs the testkit's one-call setup() once
170
+ * before the suite (connect + seed + mint/populate tokens). Sits next to
171
+ * the testkit and just delegates to it.
172
+ */
173
+ export function globalSetupContent() {
174
+ return [
175
+ `// @generated by testforge`,
176
+ `// Runs once before the Playwright suite: seeds data and mints/`,
177
+ `// populates the auth tokens the specs read (via the testkit).`,
178
+ `const testkit = require('./testkit');`,
179
+ ``,
180
+ `module.exports = async () => {`,
181
+ ` await testkit.setup();`,
182
+ `};`,
183
+ ``,
184
+ ].join('\n');
185
+ }
@@ -25,7 +25,7 @@ import { generateWithAI, generateWithAIAsync } from './ai-generator.js';
25
25
  import { generateSeed } from './seed-generator.js';
26
26
  import { planSuite } from './suite-planner.js';
27
27
  import { isSyntaxValid } from './validate.js';
28
- import { harnessInfo, generateHarnessAsync, playwrightConfig, } from './harness.js';
28
+ import { harnessInfo, generateHarnessAsync, generateHarness, playwrightConfig, globalSetupContent, } from './harness.js';
29
29
  import { hashContent, loadCache, saveCache } from './cache.js';
30
30
  import { findMarkdownFiles } from './find-files.js';
31
31
  import { loadConfig, testDirForType, resolveFramework } from './config.js';
@@ -140,6 +140,53 @@ function collisionWarning(testDir, id, ext) {
140
140
  return null;
141
141
  return `an existing test with different naming already covers this journey: ${clashes.join(', ')} — new file may duplicate it`;
142
142
  }
143
+ /**
144
+ * (Re)generate ONLY the shared harness for one or all areas — without
145
+ * touching any test file. Use this after upgrading the harness (e.g. to
146
+ * add token-minting): the new testkit populates process.env for the token
147
+ * vars the existing tests read, so they run unchanged. Cheap: one AI call
148
+ * per area, no test regeneration.
149
+ */
150
+ export async function runHarness(only) {
151
+ const config = loadConfig();
152
+ const ext = config.testExtension ?? '.test.ts';
153
+ const areas = Object.keys(config.testOutput ?? {});
154
+ const written = [];
155
+ for (const area of areas) {
156
+ if (only && area !== only)
157
+ continue;
158
+ const areaDir = testDirForType(config, area);
159
+ const framework = resolveFramework(config, area, area);
160
+ const info = harnessInfo(areaDir, framework, ext);
161
+ let kit;
162
+ try {
163
+ kit = generateHarness(info.path, framework);
164
+ }
165
+ catch {
166
+ console.log(` harness generation failed for area "${area}" — skipped`);
167
+ continue;
168
+ }
169
+ if (!isSyntaxValid(kit, ext.endsWith('.js') ? '.js' : '.ts')) {
170
+ console.log(` harness for area "${area}" did not parse — skipped`);
171
+ continue;
172
+ }
173
+ mkdirSync(dirname(info.path), { recursive: true });
174
+ writeFileSync(info.path, kit, 'utf-8');
175
+ if (framework === 'playwright') {
176
+ const gsPath = join(info.dir, 'global-setup.js');
177
+ writeFileSync(gsPath, globalSetupContent(), 'utf-8');
178
+ const pkgRoot = dirname(areaDir);
179
+ const cfg = join(pkgRoot, 'playwright.config.js');
180
+ if (!existsSync(cfg)) {
181
+ const rel = areaDir.slice(pkgRoot.length + 1) || '.';
182
+ const gsRel = gsPath.slice(pkgRoot.length + 1);
183
+ writeFileSync(cfg, playwrightConfig(`./${rel}`, `./${gsRel}`), 'utf-8');
184
+ }
185
+ }
186
+ written.push(info.path);
187
+ }
188
+ return written;
189
+ }
143
190
  /** Does this doc file / journey match a --journey selector? */
144
191
  function matchesJourney(file, journeyId, sel) {
145
192
  const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-');
@@ -175,13 +222,17 @@ export async function runGenerate(docsDir, useAI = true, force = false, only) {
175
222
  }
176
223
  mkdirSync(dirname(info.path), { recursive: true });
177
224
  writeFileSync(info.path, kit, 'utf-8');
178
- // Playwright areas need a config so the specs can actually run.
225
+ // Playwright areas need a config + globalSetup so the specs can run
226
+ // AND get their tokens minted once before the suite.
179
227
  if (framework === 'playwright') {
228
+ const gsPath = join(info.dir, 'global-setup.js');
229
+ writeFileSync(gsPath, globalSetupContent(), 'utf-8');
180
230
  const pkgRoot = dirname(areaDir);
181
231
  const cfg = join(pkgRoot, 'playwright.config.js');
182
232
  if (!existsSync(cfg)) {
183
233
  const rel = areaDir.slice(pkgRoot.length + 1) || '.';
184
- writeFileSync(cfg, playwrightConfig(`./${rel}`), 'utf-8');
234
+ const gsRel = gsPath.slice(pkgRoot.length + 1);
235
+ writeFileSync(cfg, playwrightConfig(`./${rel}`, `./${gsRel}`), 'utf-8');
185
236
  }
186
237
  }
187
238
  harnessByArea.set(area, info.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@activate-core/test-forge",
3
- "version": "0.7.0",
3
+ "version": "0.8.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": {