@activate-core/test-forge 0.10.2 → 0.12.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 +129 -28
- package/dist/core/config.js +2 -0
- package/dist/core/detect.js +210 -0
- package/dist/core/harness.js +159 -11
- package/dist/core/playwright-writer.js +53 -38
- package/dist/core/run-generate.js +225 -20
- package/dist/core/seed-generator.js +16 -5
- package/dist/core/validate.js +219 -0
- package/package.json +1 -1
package/dist/core/harness.js
CHANGED
|
@@ -24,20 +24,57 @@ const MAP_PATH = 'testforge.md';
|
|
|
24
24
|
export const HARNESS_CONTRACT = [
|
|
25
25
|
`The testkit module MUST export exactly this stable API (CommonJS`,
|
|
26
26
|
`module.exports), implemented against the project's real data layer:`,
|
|
27
|
-
` - connect(): Promise<void> open the DB connection
|
|
28
|
-
`
|
|
27
|
+
` - connect(): Promise<void> open the DB connection(s). Some apps`,
|
|
28
|
+
` span MULTIPLE databases (e.g. a main DB,`,
|
|
29
|
+
` a separate chat/conversation DB, and an`,
|
|
30
|
+
` identity/IM DB) — READ the real data`,
|
|
31
|
+
` config to find how many and their env-var`,
|
|
32
|
+
` names, and connect to EACH. Do NOT assume`,
|
|
33
|
+
` a single process.env.DB_URL.`,
|
|
29
34
|
` - disconnect(): Promise<void> close it`,
|
|
30
|
-
` - reset(): Promise<void> remove
|
|
31
|
-
`
|
|
35
|
+
` - reset(): Promise<void> remove ONLY test data (records with the`,
|
|
36
|
+
` "qa-"/"QA-TEST" prefix or the well-known`,
|
|
37
|
+
` TEST_IDS), cascading children by`,
|
|
38
|
+
` projectId. MUST be gated — see SAFETY.`,
|
|
32
39
|
` - seed(spec?): Promise<object> idempotently upsert the test records`,
|
|
33
40
|
` the tests need (using the real models`,
|
|
34
41
|
` /collections + enums), returning the`,
|
|
35
|
-
` created ids/handles`,
|
|
36
|
-
`
|
|
42
|
+
` created ids/handles. Seed EACH record`,
|
|
43
|
+
` into the database it actually belongs to`,
|
|
44
|
+
` (e.g. the identity/IM user in the IM DB,`,
|
|
45
|
+
` a conversation in the chat DB) — not all`,
|
|
46
|
+
` into one DB. Upsert with { $set: mutable,`,
|
|
47
|
+
` $setOnInsert: { createdAt } } — never`,
|
|
48
|
+
` replaceOne, never clobber creation`,
|
|
49
|
+
` metadata or a pre-existing richer record.`,
|
|
50
|
+
` Seeds are TRUSTED fixtures: do NOT let the`,
|
|
51
|
+
` app's own schema quirks block them — if an`,
|
|
52
|
+
` update-validator rejects a value the app`,
|
|
53
|
+
` itself defaults (e.g. a required String`,
|
|
54
|
+
` whose default is ''), write WITHOUT update`,
|
|
55
|
+
` validators (runValidators:false) rather`,
|
|
56
|
+
` than failing the whole seed.`,
|
|
57
|
+
` - TEST_IDS: object the stable well-known test ids/handles.`,
|
|
58
|
+
` EVERY id MUST be env-overridable: resolve`,
|
|
59
|
+
` each as process.env.TEST_<NAME>_ID ||`,
|
|
60
|
+
` '<stable default>' (e.g. TEST_PROJECT_ID,`,
|
|
61
|
+
` TEST_WEARER_ID). This lets a caller point`,
|
|
62
|
+
` the suite at data created BY HAND on`,
|
|
63
|
+
` staging/prod (with SEED_MODE=existing)`,
|
|
64
|
+
` without editing code. Resolve non-id`,
|
|
65
|
+
` handles (phone, email, auth subject) the`,
|
|
66
|
+
` same env-overridable way.`,
|
|
37
67
|
` - setup(): Promise<void> THE one-call entry point (idempotent,`,
|
|
38
68
|
` safe to call from every beforeAll AND`,
|
|
39
69
|
` from a Playwright globalSetup). It:`,
|
|
40
|
-
` 1. connect()s
|
|
70
|
+
` 1. connect()s; then seed()s — UNLESS`,
|
|
71
|
+
` process.env.SEED_MODE === 'existing',`,
|
|
72
|
+
` in which case it SKIPS seed() and`,
|
|
73
|
+
` reset() and runs against data already`,
|
|
74
|
+
` in the DB (staging/prod, where the`,
|
|
75
|
+
` fixtures were created by hand and`,
|
|
76
|
+
` addressed via the env-overridable`,
|
|
77
|
+
` TEST_IDS);`,
|
|
41
78
|
` 2. MINTS a fresh auth access token at`,
|
|
42
79
|
` runtime — read the project's REAL`,
|
|
43
80
|
` login/refresh auth (controllers/`,
|
|
@@ -64,11 +101,66 @@ export const HARNESS_CONTRACT = [
|
|
|
64
101
|
` fixture — \`const { request: apiRequest } = require('@playwright/`,
|
|
65
102
|
` test'); return apiRequest.newContext({ baseURL, extraHTTPHeaders })\`.`,
|
|
66
103
|
` The \`request\` FIXTURE has NO .newContext(); calling it throws.`,
|
|
67
|
-
` - login(page): Promise<void>
|
|
104
|
+
` - login(page): Promise<void> establish an authenticated UI session`,
|
|
105
|
+
` the webapp actually accepts. Inject the`,
|
|
106
|
+
` SAME real minted access token (a valid,`,
|
|
107
|
+
` decodable JWT — the app gate typically`,
|
|
108
|
+
` jwtDecode()s it and checks exp) into the`,
|
|
109
|
+
` EXACT localStorage/cookie keys the app`,
|
|
110
|
+
` reads (GREP the auth reducer/guard for the`,
|
|
111
|
+
` key names). A fabricated/opaque token that`,
|
|
112
|
+
` fails the app's validity check leaves the`,
|
|
113
|
+
` user logged-out (redirect loop). The token`,
|
|
114
|
+
` owner MUST be the identity that owns the`,
|
|
115
|
+
` seeded fixtures (see IDENTITY BINDING), so`,
|
|
116
|
+
` the UI shows the seeded data.`,
|
|
68
117
|
`Use only env var NAMES for secrets/connection — never literal values.`,
|
|
69
118
|
`The ONLY credentials a developer should have to provide are the DB`,
|
|
70
119
|
`connection, a Stripe test key, and ONE seed/refresh token (or seeded`,
|
|
71
120
|
`login) — everything else setup() derives at runtime.`,
|
|
121
|
+
``,
|
|
122
|
+
`SAFETY — this testkit must NEVER be able to damage a real environment:`,
|
|
123
|
+
` - reset() and ANY delete/drop MUST refuse unless BOTH hold: the opt-in`,
|
|
124
|
+
` flag process.env.TESTFORGE_ALLOW_RESET === 'true' is set, AND the`,
|
|
125
|
+
` connection is clearly NOT production (e.g. host is localhost or the`,
|
|
126
|
+
` DB name contains "test"). Otherwise throw a clear Error naming the`,
|
|
127
|
+
` target. Default is SAFE: with the flag unset, reset() throws and`,
|
|
128
|
+
` deletes nothing.`,
|
|
129
|
+
` - When process.env.SEED_MODE === 'existing', reset() and all seed()`,
|
|
130
|
+
` WRITES are skipped entirely — the suite runs against data already`,
|
|
131
|
+
` present (hand-created on staging/prod), addressed via TEST_IDS. Do`,
|
|
132
|
+
` not mutate that data destructively.`,
|
|
133
|
+
` - Never widen a delete beyond the "qa-"/"QA-TEST" prefix + TEST_IDS`,
|
|
134
|
+
` scope; always cascade children by projectId, never a bare drop.`,
|
|
135
|
+
``,
|
|
136
|
+
`IDENTITY BINDING — the #1 cause of false failures, get this right.`,
|
|
137
|
+
`Owner-scoped endpoints ("my bookings", "my payouts") derive the owner`,
|
|
138
|
+
`from the AUTHENTICATED TOKEN, not from a query param — and a route`,
|
|
139
|
+
`declared auth:false may STILL populate request.user via a pre-auth`,
|
|
140
|
+
`extension when a Bearer is sent. Therefore:`,
|
|
141
|
+
` - setup() MUST resolve the authenticated identity's REAL id (decode`,
|
|
142
|
+
` the minted token's subject/_id claim, or call the profile/whoami`,
|
|
143
|
+
` endpoint) and seed the OWNER fixtures (the maker/wearer/customer`,
|
|
144
|
+
` whose data the tests read) under THAT id — then set the TEST_IDS`,
|
|
145
|
+
` owner ids to it.`,
|
|
146
|
+
` - Do NOT hardcode an owner id the token does not own: the seeded rows`,
|
|
147
|
+
` would never be returned to the authenticated caller, and every`,
|
|
148
|
+
` owner-scoped test would fail for a fake reason.`,
|
|
149
|
+
` - A scope/IDOR test needing a SECOND, non-owner identity needs a`,
|
|
150
|
+
` distinct token (env). If only one identity is available, make those`,
|
|
151
|
+
` cases pending — never assert a scope the token cannot actually prove.`,
|
|
152
|
+
``,
|
|
153
|
+
`CONNECTION LIFECYCLE — setup() must be safe to call repeatedly across`,
|
|
154
|
+
`suites, or one suite's teardown breaks the next (MongoNotConnectedError).`,
|
|
155
|
+
` - connect() and setup() MUST check the live connection state before`,
|
|
156
|
+
` use (e.g. mongoose.connection.readyState !== 1) and REOPEN if closed.`,
|
|
157
|
+
` Do NOT gate reconnection behind a memoized "already set up" promise —`,
|
|
158
|
+
` a cached promise that resolved once will NOT reopen a since-closed`,
|
|
159
|
+
` connection. Guard on the ACTUAL connection state, not a boolean flag.`,
|
|
160
|
+
` - Generated tests must NOT call disconnect() on the shared connection`,
|
|
161
|
+
` in an afterAll/afterEach. Leave teardown to a single global teardown`,
|
|
162
|
+
` (Playwright globalTeardown / Jest globalTeardown) that runs ONCE after`,
|
|
163
|
+
` the whole run — never per-file.`,
|
|
72
164
|
].join('\n');
|
|
73
165
|
/** Where an area's shared testkit lives, and how tests import it. */
|
|
74
166
|
export function harnessInfo(areaDir, framework, ext) {
|
|
@@ -96,7 +188,10 @@ function buildHarnessPrompt(harnessPath, framework) {
|
|
|
96
188
|
`You are inside the repository with read-only tools (Read, Grep, Glob,`,
|
|
97
189
|
`LS). READ the real DB config, the models/schemas, AND the real auth`,
|
|
98
190
|
`flow (login / refresh-token / OTP controllers + middleware) so setup()`,
|
|
99
|
-
`mints tokens the way the app actually does.
|
|
191
|
+
`mints tokens the way the app actually does. Determine whether the app`,
|
|
192
|
+
`uses MORE THAN ONE database (e.g. a main DB, a chat/conversation DB, an`,
|
|
193
|
+
`identity/IM DB); if so, connect to and seed EACH via its own env-driven`,
|
|
194
|
+
`connection — do not collapse them into one. Also GREP the sibling test`,
|
|
100
195
|
`files next to this testkit for every process.env.<NAME> they read`,
|
|
101
196
|
`(especially *TOKEN*, *_ID, base URLs) and make setup() POPULATE those`,
|
|
102
197
|
`exact names — the tests must run without any hand-pasted token.`,
|
|
@@ -107,8 +202,10 @@ function buildHarnessPrompt(harnessPath, framework) {
|
|
|
107
202
|
`Because this is a JEST/backend area, do NOT require or import`,
|
|
108
203
|
`'@playwright/test' anywhere (it is not installed in a backend`,
|
|
109
204
|
`package — a top-level require would break every suite that loads`,
|
|
110
|
-
`this testkit). OMIT the Playwright-only apiContext/login exports
|
|
111
|
-
`
|
|
205
|
+
`this testkit). OMIT the Playwright-only apiContext/login exports.`,
|
|
206
|
+
`Backend suites drive the server in-process via the framework's own`,
|
|
207
|
+
`inject (or an already-installed HTTP client) — never Playwright,`,
|
|
208
|
+
`and never a library this project has not installed.`,
|
|
112
209
|
``,
|
|
113
210
|
]
|
|
114
211
|
: []),
|
|
@@ -197,3 +294,54 @@ export function globalSetupContent() {
|
|
|
197
294
|
``,
|
|
198
295
|
].join('\n');
|
|
199
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* A fixture/environment guide written next to the testkit. Documents the
|
|
299
|
+
* two run modes and — crucially — how to run against data created BY HAND
|
|
300
|
+
* on staging/prod (env-addressable TEST_IDS + SEED_MODE=existing), plus the
|
|
301
|
+
* safety gates. Deterministic and project-agnostic; the exact per-entity
|
|
302
|
+
* shape lives in the generated seed()/TEST_IDS.
|
|
303
|
+
*/
|
|
304
|
+
export function fixturesDoc() {
|
|
305
|
+
return [
|
|
306
|
+
`# Test fixtures & environments`,
|
|
307
|
+
``,
|
|
308
|
+
`Generated by testforge. The same tests run in two modes.`,
|
|
309
|
+
``,
|
|
310
|
+
`## Modes`,
|
|
311
|
+
`- **auto** (default — local / CI): the testkit seeds its own fixtures`,
|
|
312
|
+
` into a throwaway TEST database and resets them between runs. No manual`,
|
|
313
|
+
` setup.`,
|
|
314
|
+
`- **existing** (staging / production): the testkit does NOT seed or`,
|
|
315
|
+
` reset — it runs against data that already exists. Enable with:`,
|
|
316
|
+
``,
|
|
317
|
+
` SEED_MODE=existing`,
|
|
318
|
+
``,
|
|
319
|
+
`## Running against data you created by hand (staging / prod)`,
|
|
320
|
+
`1. Create the fixture entities in the target database yourself (a`,
|
|
321
|
+
` project, its price line(s), the wearer/maker users, etc.) — mirror`,
|
|
322
|
+
` what the seed builders in \`helpers/testkit\` produce.`,
|
|
323
|
+
`2. Point the suite at them — every well-known id is env-overridable:`,
|
|
324
|
+
``,
|
|
325
|
+
` TEST_PROJECT_ID=<your project id>`,
|
|
326
|
+
` TEST_WEARER_ID=<...> # and any other TEST_*_ID the suite reads`,
|
|
327
|
+
``,
|
|
328
|
+
`3. Provide auth + connection env (DB url/name(s), a seed/refresh token`,
|
|
329
|
+
` or seeded login, Stripe test key).`,
|
|
330
|
+
`4. Run with \`SEED_MODE=existing\` so nothing is seeded or deleted.`,
|
|
331
|
+
``,
|
|
332
|
+
`## Safety`,
|
|
333
|
+
`- \`reset()\` and any delete are DISABLED unless BOTH hold:`,
|
|
334
|
+
` \`TESTFORGE_ALLOW_RESET=true\` AND the target DB is clearly`,
|
|
335
|
+
` non-production (localhost, or a DB name containing "test"). Otherwise`,
|
|
336
|
+
` it throws.`,
|
|
337
|
+
`- In \`SEED_MODE=existing\`, seeding and reset are skipped entirely —`,
|
|
338
|
+
` your data is never mutated destructively.`,
|
|
339
|
+
`- NEVER point auto mode at a production database.`,
|
|
340
|
+
``,
|
|
341
|
+
`## The exact fixture shape`,
|
|
342
|
+
`The precise fields/enums each fixture needs are defined by \`seed()\``,
|
|
343
|
+
`and \`TEST_IDS\` in \`helpers/testkit\`. Read those to reproduce a`,
|
|
344
|
+
`matching entity by hand.`,
|
|
345
|
+
``,
|
|
346
|
+
].join('\n');
|
|
347
|
+
}
|
|
@@ -1,47 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* testforge —
|
|
2
|
+
* testforge — deferred-stub writer (fallback only).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* When AI generation is unavailable (usage limit / repeated invalid
|
|
5
|
+
* output), testforge still needs to leave SOMETHING at the file path so
|
|
6
|
+
* the journey is visibly accounted for. It must NEVER be a test that
|
|
7
|
+
* runs and passes with no assertions — a silent false-green is the worst
|
|
8
|
+
* QA outcome. So this writer emits PENDING placeholders only:
|
|
9
|
+
* - Jest → test.todo(...) (pending; no assertions; no import)
|
|
10
|
+
* - Playwright → test.fixme(...) (skipped; never counts as a pass)
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @generated so it is easy to spot auto-created tests.
|
|
12
|
+
* It is also runner-aware: a Jest area never receives a
|
|
13
|
+
* `@playwright/test` import (which would break every suite that loads).
|
|
14
|
+
* A developer (or a later AI run) replaces these with real tests.
|
|
13
15
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
const BANNER = [
|
|
17
|
+
`// @generated by testforge — DEFERRED stub (AI generation was unavailable).`,
|
|
18
|
+
`// These are PLACEHOLDERS, not real tests: they are pending so they do`,
|
|
19
|
+
`// NOT pass silently. Regenerate with AI once available, or implement`,
|
|
20
|
+
`// them by hand.`,
|
|
21
|
+
].join('\n');
|
|
22
|
+
/** One pending entry, phrased for the area's runner. */
|
|
23
|
+
function pendingLine(testCase, framework) {
|
|
24
|
+
const outcome = testCase.expect === 'pass' ? 'succeed' : 'be rejected';
|
|
25
|
+
const title = `${testCase.title} — TODO: implement (expected to ${outcome})`;
|
|
26
|
+
const safe = title.replace(/"/g, '\\"');
|
|
27
|
+
return framework === 'jest'
|
|
28
|
+
? ` test.todo("${safe}");`
|
|
29
|
+
: ` test.fixme("${safe}", async () => {});`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the full deferred-stub file for a journey, correct for the area's
|
|
33
|
+
* test runner. Every entry is pending, so the file contributes zero
|
|
34
|
+
* false-passing tests.
|
|
35
|
+
*/
|
|
36
|
+
export function writeDeferredStub(journey, cases, framework) {
|
|
37
|
+
const entries = (cases.length ? cases : [{ title: journey.name, expect: 'pass' }])
|
|
38
|
+
.map((c) => pendingLine(c, framework))
|
|
39
|
+
.join('\n');
|
|
40
|
+
if (framework === 'jest') {
|
|
41
|
+
// Jest globals — no import (a `@playwright/test` import here would
|
|
42
|
+
// break the whole suite in a backend package).
|
|
43
|
+
return [
|
|
44
|
+
BANNER,
|
|
45
|
+
``,
|
|
46
|
+
`describe(${JSON.stringify(journey.name)}, () => {`,
|
|
47
|
+
entries,
|
|
48
|
+
`});`,
|
|
49
|
+
``,
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
16
52
|
return [
|
|
17
|
-
`import { test
|
|
53
|
+
`import { test } from "@playwright/test";`,
|
|
54
|
+
``,
|
|
55
|
+
BANNER,
|
|
18
56
|
``,
|
|
19
|
-
|
|
20
|
-
|
|
57
|
+
`test.describe(${JSON.stringify(journey.name)}, () => {`,
|
|
58
|
+
entries,
|
|
59
|
+
`});`,
|
|
21
60
|
``,
|
|
22
61
|
].join('\n');
|
|
23
62
|
}
|
|
24
|
-
/** Build a single Playwright test block from one test case. */
|
|
25
|
-
function testBlock(journey, testCase) {
|
|
26
|
-
const lines = [];
|
|
27
|
-
lines.push(` // @generated`);
|
|
28
|
-
lines.push(` test("${testCase.title}", async ({ page }) => {`);
|
|
29
|
-
lines.push(` await page.goto("${journey.route}");`);
|
|
30
|
-
lines.push(``);
|
|
31
|
-
lines.push(` // TODO: fill in the steps for this test.`);
|
|
32
|
-
if (testCase.expect === 'pass') {
|
|
33
|
-
lines.push(` // This case is expected to succeed.`);
|
|
34
|
-
lines.push(` // TODO: assert the success outcome (e.g. redirect or success message).`);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
lines.push(` // This case is expected to fail / be rejected.`);
|
|
38
|
-
lines.push(` // TODO: assert the error outcome (e.g. validation message shown).`);
|
|
39
|
-
}
|
|
40
|
-
lines.push(` });`);
|
|
41
|
-
return lines.join('\n');
|
|
42
|
-
}
|
|
43
|
-
/** Build the full Playwright test file content for a journey. */
|
|
44
|
-
export function writePlaywrightFile(journey, cases) {
|
|
45
|
-
const blocks = cases.map((c) => testBlock(journey, c)).join('\n\n');
|
|
46
|
-
return header(journey) + blocks + `\n\n});\n`;
|
|
47
|
-
}
|