@gate-forge/pack-playwright 0.7.1 → 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/README.md +31 -0
- package/dist/constants.d.ts +13 -140
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +13 -148
- package/dist/constants.js.map +1 -1
- package/dist/cypress/plugin.d.ts +53 -0
- package/dist/cypress/plugin.d.ts.map +1 -0
- package/dist/cypress/plugin.js +369 -0
- package/dist/cypress/plugin.js.map +1 -0
- package/dist/cypress/spec-scan.d.ts +39 -0
- package/dist/cypress/spec-scan.d.ts.map +1 -0
- package/dist/cypress/spec-scan.js +301 -0
- package/dist/cypress/spec-scan.js.map +1 -0
- package/dist/diagnosis.d.ts +39 -0
- package/dist/diagnosis.d.ts.map +1 -0
- package/dist/diagnosis.js +63 -0
- package/dist/diagnosis.js.map +1 -0
- package/dist/discovery/adapter-projection.d.ts +13 -0
- package/dist/discovery/adapter-projection.d.ts.map +1 -0
- package/dist/discovery/adapter-projection.js +84 -0
- package/dist/discovery/adapter-projection.js.map +1 -0
- package/dist/discovery/config-locations.d.ts +5 -0
- package/dist/discovery/config-locations.d.ts.map +1 -0
- package/dist/discovery/config-locations.js +19 -0
- package/dist/discovery/config-locations.js.map +1 -0
- package/dist/discovery/cypress-runner-adapter.d.ts +112 -0
- package/dist/discovery/cypress-runner-adapter.d.ts.map +1 -0
- package/dist/discovery/cypress-runner-adapter.js +501 -0
- package/dist/discovery/cypress-runner-adapter.js.map +1 -0
- package/dist/discovery/discover.d.ts +53 -1
- package/dist/discovery/discover.d.ts.map +1 -1
- package/dist/discovery/discover.js +100 -3
- package/dist/discovery/discover.js.map +1 -1
- package/dist/discovery/git-ignore.d.ts +13 -0
- package/dist/discovery/git-ignore.d.ts.map +1 -0
- package/dist/discovery/git-ignore.js +62 -0
- package/dist/discovery/git-ignore.js.map +1 -0
- package/dist/discovery/index.d.ts +9 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +9 -0
- package/dist/discovery/index.js.map +1 -1
- package/dist/discovery/playwright-runner-adapter.d.ts +108 -0
- package/dist/discovery/playwright-runner-adapter.d.ts.map +1 -0
- package/dist/discovery/playwright-runner-adapter.js +185 -0
- package/dist/discovery/playwright-runner-adapter.js.map +1 -0
- package/dist/discovery/prepare-barrier.d.ts +522 -0
- package/dist/discovery/prepare-barrier.d.ts.map +1 -0
- package/dist/discovery/prepare-barrier.js +739 -0
- package/dist/discovery/prepare-barrier.js.map +1 -0
- package/dist/discovery/pytest-adapter.d.ts.map +1 -1
- package/dist/discovery/pytest-adapter.js +6 -2
- package/dist/discovery/pytest-adapter.js.map +1 -1
- package/dist/discovery/pytest-runner-adapter.d.ts +124 -0
- package/dist/discovery/pytest-runner-adapter.d.ts.map +1 -0
- package/dist/discovery/pytest-runner-adapter.js +454 -0
- package/dist/discovery/pytest-runner-adapter.js.map +1 -0
- package/dist/discovery/reconcile.d.ts +80 -11
- package/dist/discovery/reconcile.d.ts.map +1 -1
- package/dist/discovery/reconcile.js +395 -78
- package/dist/discovery/reconcile.js.map +1 -1
- package/dist/discovery/runner-env.d.ts +88 -2
- package/dist/discovery/runner-env.d.ts.map +1 -1
- package/dist/discovery/runner-env.js +142 -4
- package/dist/discovery/runner-env.js.map +1 -1
- package/dist/discovery/static-discovery.d.ts +38 -3
- package/dist/discovery/static-discovery.d.ts.map +1 -1
- package/dist/discovery/static-discovery.js +454 -15
- package/dist/discovery/static-discovery.js.map +1 -1
- package/dist/discovery/supervised-run.d.ts +199 -10
- package/dist/discovery/supervised-run.d.ts.map +1 -1
- package/dist/discovery/supervised-run.js +350 -39
- package/dist/discovery/supervised-run.js.map +1 -1
- package/dist/discovery/trusted-config.d.ts +180 -6
- package/dist/discovery/trusted-config.d.ts.map +1 -1
- package/dist/discovery/trusted-config.js +279 -10
- package/dist/discovery/trusted-config.js.map +1 -1
- package/dist/discovery/vitest-runner-adapter.d.ts +172 -0
- package/dist/discovery/vitest-runner-adapter.d.ts.map +1 -0
- package/dist/discovery/vitest-runner-adapter.js +607 -0
- package/dist/discovery/vitest-runner-adapter.js.map +1 -0
- package/dist/fixture/consumer-runner.d.ts +4 -0
- package/dist/fixture/consumer-runner.d.ts.map +1 -0
- package/dist/fixture/consumer-runner.js +38 -0
- package/dist/fixture/consumer-runner.js.map +1 -0
- package/dist/fixture/evidence.d.ts +27 -0
- package/dist/fixture/evidence.d.ts.map +1 -1
- package/dist/fixture/evidence.js +12 -0
- package/dist/fixture/evidence.js.map +1 -1
- package/dist/fixture/fixture.d.ts +2 -1
- package/dist/fixture/fixture.d.ts.map +1 -1
- package/dist/fixture/fixture.js +1 -17
- package/dist/fixture/fixture.js.map +1 -1
- package/dist/fixture/witness-client.d.ts +6 -174
- package/dist/fixture/witness-client.d.ts.map +1 -1
- package/dist/fixture/witness-client.js +6 -267
- package/dist/fixture/witness-client.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +6 -12
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +6 -17
- package/dist/json.js.map +1 -1
- package/dist/reporter/project-graph-reporter.d.ts +92 -0
- package/dist/reporter/project-graph-reporter.d.ts.map +1 -0
- package/dist/reporter/project-graph-reporter.js +92 -0
- package/dist/reporter/project-graph-reporter.js.map +1 -0
- package/dist/reporter/reporter.cjs.map +1 -1
- package/dist/reporter/reporter.d.ts +93 -4
- package/dist/reporter/reporter.d.ts.map +1 -1
- package/dist/reporter/reporter.js +174 -18
- package/dist/reporter/reporter.js.map +1 -1
- package/dist/runner-claims.d.ts +13 -0
- package/dist/runner-claims.d.ts.map +1 -0
- package/dist/runner-claims.js +46 -0
- package/dist/runner-claims.js.map +1 -0
- package/dist/runner-resolution.d.ts +3 -0
- package/dist/runner-resolution.d.ts.map +1 -0
- package/dist/runner-resolution.js +14 -0
- package/dist/runner-resolution.js.map +1 -0
- package/dist/supervisor/client.d.ts +23 -1
- package/dist/supervisor/client.d.ts.map +1 -1
- package/dist/supervisor/client.js +49 -0
- package/dist/supervisor/client.js.map +1 -1
- package/dist/supervisor/drain.d.ts +65 -0
- package/dist/supervisor/drain.d.ts.map +1 -1
- package/dist/supervisor/drain.js +353 -9
- package/dist/supervisor/drain.js.map +1 -1
- package/dist/supervisor/index.d.ts +7 -3
- package/dist/supervisor/index.d.ts.map +1 -1
- package/dist/supervisor/index.js +7 -3
- package/dist/supervisor/index.js.map +1 -1
- package/dist/supervisor/spool.d.ts +65 -2
- package/dist/supervisor/spool.d.ts.map +1 -1
- package/dist/supervisor/spool.js +47 -0
- package/dist/supervisor/spool.js.map +1 -1
- package/dist/surface.d.ts +6 -207
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +6 -264
- package/dist/surface.js.map +1 -1
- package/dist/vitest/index.d.ts +23 -0
- package/dist/vitest/index.d.ts.map +1 -0
- package/dist/vitest/index.js +356 -0
- package/dist/vitest/index.js.map +1 -0
- package/dist/vitest/reporter.d.ts +83 -0
- package/dist/vitest/reporter.d.ts.map +1 -0
- package/dist/vitest/reporter.js +211 -0
- package/dist/vitest/reporter.js.map +1 -0
- package/dist/vitest/worker-slot.d.ts +36 -0
- package/dist/vitest/worker-slot.d.ts.map +1 -0
- package/dist/vitest/worker-slot.js +51 -0
- package/dist/vitest/worker-slot.js.map +1 -0
- package/dist/witness/adapter-registry.d.ts +6 -50
- package/dist/witness/adapter-registry.d.ts.map +1 -1
- package/dist/witness/adapter-registry.js +6 -257
- package/dist/witness/adapter-registry.js.map +1 -1
- package/dist/witness/behavior-request.d.ts +6 -76
- package/dist/witness/behavior-request.d.ts.map +1 -1
- package/dist/witness/behavior-request.js +6 -273
- package/dist/witness/behavior-request.js.map +1 -1
- package/dist/witness/behavior.d.ts +6 -48
- package/dist/witness/behavior.d.ts.map +1 -1
- package/dist/witness/behavior.js +6 -114
- package/dist/witness/behavior.js.map +1 -1
- package/dist/witness/bin.d.ts +6 -17
- package/dist/witness/bin.d.ts.map +1 -1
- package/dist/witness/bin.js +6 -168
- package/dist/witness/bin.js.map +1 -1
- package/dist/witness/browser.d.ts +14 -206
- package/dist/witness/browser.d.ts.map +1 -1
- package/dist/witness/browser.js +15 -654
- package/dist/witness/browser.js.map +1 -1
- package/dist/witness/classifications.d.ts +6 -38
- package/dist/witness/classifications.d.ts.map +1 -1
- package/dist/witness/classifications.js +6 -83
- package/dist/witness/classifications.js.map +1 -1
- package/dist/witness/env-attestation.d.ts +6 -94
- package/dist/witness/env-attestation.d.ts.map +1 -1
- package/dist/witness/env-attestation.js +6 -198
- package/dist/witness/env-attestation.js.map +1 -1
- package/dist/witness/fixture-provider.d.ts +6 -82
- package/dist/witness/fixture-provider.d.ts.map +1 -1
- package/dist/witness/fixture-provider.js +6 -105
- package/dist/witness/fixture-provider.js.map +1 -1
- package/dist/witness/loopback-pins.d.ts +6 -75
- package/dist/witness/loopback-pins.d.ts.map +1 -1
- package/dist/witness/loopback-pins.js +6 -240
- package/dist/witness/loopback-pins.js.map +1 -1
- package/dist/witness/server.d.ts +8 -26
- package/dist/witness/server.d.ts.map +1 -1
- package/dist/witness/server.js +24 -3761
- package/dist/witness/server.js.map +1 -1
- package/dist/witness/types.d.ts +6 -882
- package/dist/witness/types.d.ts.map +1 -1
- package/dist/witness/types.js +9 -1
- package/dist/witness/types.js.map +1 -1
- package/examples/overlay-proof.spec.js +75 -0
- package/package.json +8 -2
- package/python/__pycache__/gateforge_persistence_intents.cpython-312.pyc +0 -0
- package/python/__pycache__/gateforge_persistence_intents.cpython-314.pyc +0 -0
- package/python/gateforge_pytest_plugin.py +462 -0
|
@@ -33,6 +33,8 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
|
33
33
|
import { join, posix } from 'node:path';
|
|
34
34
|
import ts from 'typescript';
|
|
35
35
|
import { pathInScope } from '@gate-forge/core';
|
|
36
|
+
import { CLAIM_ANNOTATION_TYPE } from '../constants.js';
|
|
37
|
+
import { gitIgnoredPaths } from './git-ignore.js';
|
|
36
38
|
/** Default cap on files pulled in through import traversal. */
|
|
37
39
|
export const DEFAULT_MAX_TRAVERSED_FILES = 200;
|
|
38
40
|
/** Default bound on import-traversal depth (chain length). */
|
|
@@ -125,12 +127,127 @@ function locationOf(file, source, node) {
|
|
|
125
127
|
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
126
128
|
return { file, line: line + 1, col: character };
|
|
127
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Tests whether a syntax node is nested inside another node's subtree.
|
|
132
|
+
*
|
|
133
|
+
* Args:
|
|
134
|
+
* container: candidate ancestor node.
|
|
135
|
+
* target: node whose ancestry is checked.
|
|
136
|
+
*
|
|
137
|
+
* Returns:
|
|
138
|
+
* boolean: true when target is inside container.
|
|
139
|
+
*/
|
|
140
|
+
function isWithinNode(container, target) {
|
|
141
|
+
let current = target;
|
|
142
|
+
while (current !== undefined && current !== container)
|
|
143
|
+
current = current.parent;
|
|
144
|
+
return current === container;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Finds Gateforge environment reads in conditional ancestors of a test
|
|
148
|
+
* registration so environment-dependent registrations can be reviewed.
|
|
149
|
+
*
|
|
150
|
+
* Args:
|
|
151
|
+
* file: repo-relative file containing the registration.
|
|
152
|
+
* source: parsed TypeScript source file.
|
|
153
|
+
* node: registration call node.
|
|
154
|
+
* titlePath: static title path for the registration.
|
|
155
|
+
*
|
|
156
|
+
* Returns:
|
|
157
|
+
* StaticRegistrationWarning[]: one warning per relevant environment variable.
|
|
158
|
+
*/
|
|
159
|
+
function registrationWarningsForCall(file, source, node, titlePath) {
|
|
160
|
+
const guards = [];
|
|
161
|
+
for (let parent = node.parent; parent !== undefined; parent = parent.parent) {
|
|
162
|
+
if (ts.isIfStatement(parent) &&
|
|
163
|
+
(isWithinNode(parent.thenStatement, node) ||
|
|
164
|
+
(parent.elseStatement !== undefined && isWithinNode(parent.elseStatement, node)))) {
|
|
165
|
+
guards.push(parent.expression);
|
|
166
|
+
}
|
|
167
|
+
else if (ts.isConditionalExpression(parent) &&
|
|
168
|
+
(isWithinNode(parent.whenTrue, node) || isWithinNode(parent.whenFalse, node))) {
|
|
169
|
+
guards.push(parent.condition);
|
|
170
|
+
}
|
|
171
|
+
else if (ts.isBinaryExpression(parent) &&
|
|
172
|
+
isWithinNode(parent.right, node) &&
|
|
173
|
+
(parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
174
|
+
parent.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
|
|
175
|
+
parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)) {
|
|
176
|
+
guards.push(parent.left);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (guards.length === 0)
|
|
180
|
+
return [];
|
|
181
|
+
const variables = new Map();
|
|
182
|
+
/**
|
|
183
|
+
* Recognizes property or literal-index access to process.env.
|
|
184
|
+
*
|
|
185
|
+
* Args:
|
|
186
|
+
* expression: expression to inspect.
|
|
187
|
+
*
|
|
188
|
+
* Returns:
|
|
189
|
+
* boolean: true when expression is process.env.
|
|
190
|
+
*/
|
|
191
|
+
const isProcessEnv = (expression) => (ts.isPropertyAccessExpression(expression) &&
|
|
192
|
+
ts.isIdentifier(expression.expression) &&
|
|
193
|
+
expression.expression.text === 'process' &&
|
|
194
|
+
expression.name.text === 'env') ||
|
|
195
|
+
(ts.isElementAccessExpression(expression) &&
|
|
196
|
+
ts.isIdentifier(expression.expression) &&
|
|
197
|
+
expression.expression.text === 'process' &&
|
|
198
|
+
expression.argumentExpression !== undefined &&
|
|
199
|
+
ts.isStringLiteral(expression.argumentExpression) &&
|
|
200
|
+
expression.argumentExpression.text === 'env');
|
|
201
|
+
/**
|
|
202
|
+
* Records Gateforge environment reads from one registration guard.
|
|
203
|
+
*
|
|
204
|
+
* Args:
|
|
205
|
+
* guard: conditional expression to scan.
|
|
206
|
+
*
|
|
207
|
+
* Returns:
|
|
208
|
+
* void.
|
|
209
|
+
*/
|
|
210
|
+
const visitGuard = (guard) => {
|
|
211
|
+
if (ts.isPropertyAccessExpression(guard) && isProcessEnv(guard.expression)) {
|
|
212
|
+
const name = guard.name.text;
|
|
213
|
+
if (name.startsWith('GATEFORGE_') && !variables.has(name)) {
|
|
214
|
+
variables.set(name, locationOf(file, source, guard));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (ts.isElementAccessExpression(guard) &&
|
|
218
|
+
isProcessEnv(guard.expression) &&
|
|
219
|
+
guard.argumentExpression !== undefined &&
|
|
220
|
+
ts.isStringLiteral(guard.argumentExpression)) {
|
|
221
|
+
const name = guard.argumentExpression.text;
|
|
222
|
+
if (name.startsWith('GATEFORGE_') && !variables.has(name)) {
|
|
223
|
+
variables.set(name, locationOf(file, source, guard));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
ts.forEachChild(guard, visitGuard);
|
|
227
|
+
};
|
|
228
|
+
for (const guard of guards)
|
|
229
|
+
visitGuard(guard);
|
|
230
|
+
return [...variables.entries()].map(([environmentVariable, location]) => ({
|
|
231
|
+
file,
|
|
232
|
+
titlePath: [...titlePath],
|
|
233
|
+
environmentVariable,
|
|
234
|
+
location,
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
128
237
|
/**
|
|
129
238
|
* Walks the repo (pruning VCS/deps/build dirs) and returns the sorted
|
|
130
|
-
* repo-relative posix files matching the include globs
|
|
131
|
-
*
|
|
239
|
+
* repo-relative posix files matching the include globs, no exclude glob,
|
|
240
|
+
* and no git ignore rule. Pure filesystem listing — no content is read
|
|
241
|
+
* here.
|
|
242
|
+
*
|
|
243
|
+
* A path the repository ignores (e.g. the built, git-ignored bundle a
|
|
244
|
+
* frontend build writes into the tree) can never be enumerated by the
|
|
245
|
+
* runner, so a row from it would be a permanent `unenumeratedReason`
|
|
246
|
+
* gap. Where git cannot answer — outside a work tree, or git
|
|
247
|
+
* unavailable — every candidate is kept exactly as before
|
|
248
|
+
* (see {@link gitIgnoredPaths}).
|
|
132
249
|
*/
|
|
133
|
-
function collectCandidateFiles(cwd, include, exclude) {
|
|
250
|
+
function collectCandidateFiles(cwd, include, exclude, excludeFile) {
|
|
134
251
|
const found = [];
|
|
135
252
|
const walk = (dir, rel) => {
|
|
136
253
|
let names;
|
|
@@ -157,13 +274,17 @@ function collectCandidateFiles(cwd, include, exclude) {
|
|
|
157
274
|
}
|
|
158
275
|
else if (PARSEABLE_EXTENSIONS.some((ext) => name.endsWith(ext)) &&
|
|
159
276
|
pathInScope(relPath, include) &&
|
|
160
|
-
!pathInScope(relPath, exclude)
|
|
277
|
+
!pathInScope(relPath, exclude) &&
|
|
278
|
+
excludeFile?.(relPath) !== true) {
|
|
161
279
|
found.push(relPath);
|
|
162
280
|
}
|
|
163
281
|
}
|
|
164
282
|
};
|
|
165
283
|
walk(cwd, '');
|
|
166
|
-
|
|
284
|
+
const ignored = gitIgnoredPaths(cwd, found);
|
|
285
|
+
if (ignored === null)
|
|
286
|
+
return found.sort();
|
|
287
|
+
return found.filter((file) => !ignored.has(file)).sort();
|
|
167
288
|
}
|
|
168
289
|
/** The callee shape `{base, names}` of `a.b.c(...)`, else null. */
|
|
169
290
|
function calleeChain(expression) {
|
|
@@ -206,10 +327,19 @@ function bodyReferencesTest(body) {
|
|
|
206
327
|
visit(body);
|
|
207
328
|
return found;
|
|
208
329
|
}
|
|
209
|
-
/**
|
|
330
|
+
/**
|
|
331
|
+
* Reads a file's text; unreadable content is a parse error row. A path
|
|
332
|
+
* that is not a regular file (a directory that reached the read through
|
|
333
|
+
* a seeded or traversed row) is skipped silently — a directory is not
|
|
334
|
+
* a parse failure of the consumer's source, and reporting one used to
|
|
335
|
+
* flip `inventoryComplete` on a repo that parses fine.
|
|
336
|
+
*/
|
|
210
337
|
function readText(state, file) {
|
|
338
|
+
const absolute = join(state.cwd, file);
|
|
339
|
+
if (!isFile(absolute))
|
|
340
|
+
return null;
|
|
211
341
|
try {
|
|
212
|
-
return readFileSync(
|
|
342
|
+
return readFileSync(absolute, 'utf8');
|
|
213
343
|
}
|
|
214
344
|
catch (error) {
|
|
215
345
|
state.result.parseErrors.push({
|
|
@@ -539,7 +669,25 @@ function isPlaywrightSpecifier(specifier) {
|
|
|
539
669
|
function isTestModuleSpecifier(specifier) {
|
|
540
670
|
return isPlaywrightSpecifier(specifier) || isPackSpecifier(specifier);
|
|
541
671
|
}
|
|
542
|
-
/**
|
|
672
|
+
/**
|
|
673
|
+
* Resolves a relative specifier the way node/TS resolve it: a FILE
|
|
674
|
+
* first (the bare path when it names one, then each parseable
|
|
675
|
+
* extension), then a directory's `index.*`. A directory is never a
|
|
676
|
+
* resolution result — `existsSync` is true for one, and returning it
|
|
677
|
+
* made `readText` fail with EISDIR and report a false parse error
|
|
678
|
+
* against the consumer's own source whenever a file and a directory
|
|
679
|
+
* shared a name (install rehearsal F16).
|
|
680
|
+
*
|
|
681
|
+
* Args:
|
|
682
|
+
* state: the running scan (seeded/traversed files win outright).
|
|
683
|
+
* cwd: absolute repo root.
|
|
684
|
+
* fromFile: repo-relative file holding the import.
|
|
685
|
+
* specifier: the import specifier, as written.
|
|
686
|
+
*
|
|
687
|
+
* Returns:
|
|
688
|
+
* string | null: repo-relative FILE path, or null when nothing
|
|
689
|
+
* resolves.
|
|
690
|
+
*/
|
|
543
691
|
function resolveSpecifier(state, cwd, fromFile, specifier) {
|
|
544
692
|
const base = posix.dirname(fromFile);
|
|
545
693
|
const joined = posix.normalize(posix.join(base, specifier));
|
|
@@ -549,12 +697,33 @@ function resolveSpecifier(state, cwd, fromFile, specifier) {
|
|
|
549
697
|
...PARSEABLE_EXTENSIONS.map((ext) => `${joined}/index${ext}`),
|
|
550
698
|
];
|
|
551
699
|
for (const candidate of candidates) {
|
|
552
|
-
if (state.seeded.has(candidate) || state.traversed.has(candidate)
|
|
700
|
+
if (state.seeded.has(candidate) || state.traversed.has(candidate))
|
|
701
|
+
return candidate;
|
|
702
|
+
}
|
|
703
|
+
for (const candidate of candidates) {
|
|
704
|
+
if (isFile(join(cwd, candidate)))
|
|
553
705
|
return candidate;
|
|
554
|
-
}
|
|
555
706
|
}
|
|
556
707
|
return null;
|
|
557
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* Whether an absolute path is an existing regular file (never a
|
|
711
|
+
* directory, a socket, or a device node).
|
|
712
|
+
*
|
|
713
|
+
* Args:
|
|
714
|
+
* absolute: absolute filesystem path.
|
|
715
|
+
*
|
|
716
|
+
* Returns:
|
|
717
|
+
* boolean: true only for an existing regular file.
|
|
718
|
+
*/
|
|
719
|
+
function isFile(absolute) {
|
|
720
|
+
try {
|
|
721
|
+
return statSync(absolute).isFile();
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
558
727
|
/**
|
|
559
728
|
* Resolves whether `name` in `file` binds to a playwright test function,
|
|
560
729
|
* through aliases and relative imports, bounded by depth + file budget.
|
|
@@ -696,6 +865,34 @@ function findPageRoute(node, source, file) {
|
|
|
696
865
|
visit(node);
|
|
697
866
|
return found;
|
|
698
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Collects literal URL patterns intercepted by `page.route` in one test.
|
|
870
|
+
*
|
|
871
|
+
* Args:
|
|
872
|
+
* node: the test callback subtree.
|
|
873
|
+
*
|
|
874
|
+
* Returns:
|
|
875
|
+
* string[]: unique, sorted static route patterns.
|
|
876
|
+
*/
|
|
877
|
+
function pageRouteTargets(node) {
|
|
878
|
+
const targets = new Set();
|
|
879
|
+
const visit = (current) => {
|
|
880
|
+
if (ts.isCallExpression(current) &&
|
|
881
|
+
ts.isPropertyAccessExpression(current.expression) &&
|
|
882
|
+
current.expression.name.text === 'route' &&
|
|
883
|
+
ts.isIdentifier(current.expression.expression)) {
|
|
884
|
+
const target = current.arguments[0];
|
|
885
|
+
if (target !== undefined &&
|
|
886
|
+
ts.isStringLiteralLike(target) &&
|
|
887
|
+
target.text.length > 0) {
|
|
888
|
+
targets.add(target.text);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
ts.forEachChild(current, visit);
|
|
892
|
+
};
|
|
893
|
+
visit(node);
|
|
894
|
+
return [...targets].sort();
|
|
895
|
+
}
|
|
699
896
|
/** Whether the subtree calls fetch/axios (the app's HTTP boundary). */
|
|
700
897
|
function findHttpClientCall(node, source, file) {
|
|
701
898
|
let found = null;
|
|
@@ -769,14 +966,21 @@ function findModuleMock(source, file) {
|
|
|
769
966
|
* options: cwd, include/exclude globs, and optional budgets.
|
|
770
967
|
*
|
|
771
968
|
* Returns:
|
|
772
|
-
* StaticScanResult: entries, unresolved rows, parse errors,
|
|
773
|
-
* files, and the budget flag. Never throws for
|
|
774
|
-
* problems — those are rows (fail closed as data, not silence).
|
|
969
|
+
* StaticScanResult: entries, unresolved rows, parse errors, registration
|
|
970
|
+
* warnings, scanned files, and the budget flag. Never throws for
|
|
971
|
+
* scanner-detectable problems — those are rows (fail closed as data, not silence).
|
|
775
972
|
*/
|
|
776
973
|
export function scanTestFiles(options) {
|
|
777
974
|
const state = {
|
|
778
975
|
cwd: options.cwd,
|
|
779
|
-
result: {
|
|
976
|
+
result: {
|
|
977
|
+
entries: [],
|
|
978
|
+
unresolved: [],
|
|
979
|
+
parseErrors: [],
|
|
980
|
+
registrationWarnings: [],
|
|
981
|
+
scannedFiles: [],
|
|
982
|
+
budgetExceeded: false,
|
|
983
|
+
},
|
|
780
984
|
models: new Map(),
|
|
781
985
|
seeded: new Set(),
|
|
782
986
|
traversed: new Set(),
|
|
@@ -784,7 +988,7 @@ export function scanTestFiles(options) {
|
|
|
784
988
|
maxTraversedFiles: options.budget?.maxTraversedFiles ?? DEFAULT_MAX_TRAVERSED_FILES,
|
|
785
989
|
maxImportDepth: options.budget?.maxImportDepth ?? DEFAULT_MAX_IMPORT_DEPTH,
|
|
786
990
|
};
|
|
787
|
-
const seededFiles = collectCandidateFiles(options.cwd, options.include, options.exclude);
|
|
991
|
+
const seededFiles = collectCandidateFiles(options.cwd, options.include, options.exclude, options.excludeFile);
|
|
788
992
|
for (const file of seededFiles)
|
|
789
993
|
state.seeded.add(file);
|
|
790
994
|
for (const file of seededFiles) {
|
|
@@ -796,6 +1000,10 @@ export function scanTestFiles(options) {
|
|
|
796
1000
|
const gateforgeImport = findGateforgeFixtureImport(model.source, file);
|
|
797
1001
|
scanFileForTests(state, options.cwd, model, fileHttpClient, fileMock, gateforgeImport);
|
|
798
1002
|
}
|
|
1003
|
+
state.result.registrationWarnings.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||
|
|
1004
|
+
a.location.line - b.location.line ||
|
|
1005
|
+
(a.environmentVariable < b.environmentVariable ? -1 : a.environmentVariable > b.environmentVariable ? 1 : 0) ||
|
|
1006
|
+
(a.titlePath.join('>') < b.titlePath.join('>') ? -1 : a.titlePath.join('>') > b.titlePath.join('>') ? 1 : 0));
|
|
799
1007
|
state.result.entries.sort((a, b) => compareEntry(a, b));
|
|
800
1008
|
state.result.unresolved.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||
|
|
801
1009
|
a.location.line - b.location.line ||
|
|
@@ -951,6 +1159,7 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
|
|
|
951
1159
|
gateforgeImport,
|
|
952
1160
|
});
|
|
953
1161
|
state.result.entries.push(entry);
|
|
1162
|
+
state.result.registrationWarnings.push(...registrationWarningsForCall(file, source, node, entry.titlePath));
|
|
954
1163
|
// Walk the body so inner zero-arg `test.skip()` / `test.fixme()`
|
|
955
1164
|
// calls attach to THIS entry's signals.
|
|
956
1165
|
if (callback !== undefined) {
|
|
@@ -991,6 +1200,7 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
|
|
|
991
1200
|
gateforgeImport,
|
|
992
1201
|
});
|
|
993
1202
|
state.result.entries.push(entry);
|
|
1203
|
+
state.result.registrationWarnings.push(...registrationWarningsForCall(file, source, outer, entry.titlePath));
|
|
994
1204
|
if (outerCallback !== undefined) {
|
|
995
1205
|
const enclosing = { facts: entry.facts, signals: entry.signals };
|
|
996
1206
|
ts.forEachChild(outerCallback, (child) => visit(child, describeStack, enclosing));
|
|
@@ -1034,9 +1244,234 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
|
|
|
1034
1244
|
};
|
|
1035
1245
|
visit(source, [], null);
|
|
1036
1246
|
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Finds one top-level local constant initializer by name.
|
|
1249
|
+
*
|
|
1250
|
+
* Args:
|
|
1251
|
+
* source: parsed test source file.
|
|
1252
|
+
* name: local identifier to resolve.
|
|
1253
|
+
*
|
|
1254
|
+
* Returns:
|
|
1255
|
+
* ts.Expression | null: a unique const initializer, or null when it
|
|
1256
|
+
* is absent, ambiguous, mutable, or destructured.
|
|
1257
|
+
*/
|
|
1258
|
+
function localConstantInitializer(source, name) {
|
|
1259
|
+
const matches = [];
|
|
1260
|
+
for (const statement of source.statements) {
|
|
1261
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
1262
|
+
continue;
|
|
1263
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1264
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === name && declaration.initializer !== undefined) {
|
|
1265
|
+
matches.push(declaration.initializer);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return matches.length === 1 ? (matches[0] ?? null) : null;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Finds a pure local helper declared as a function or const arrow.
|
|
1273
|
+
*
|
|
1274
|
+
* Args:
|
|
1275
|
+
* source: parsed test source file.
|
|
1276
|
+
* name: helper identifier to resolve.
|
|
1277
|
+
*
|
|
1278
|
+
* Returns:
|
|
1279
|
+
* ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | null:
|
|
1280
|
+
* one unambiguous top-level function helper.
|
|
1281
|
+
*/
|
|
1282
|
+
function localStaticFunction(source, name) {
|
|
1283
|
+
const matches = [];
|
|
1284
|
+
for (const statement of source.statements) {
|
|
1285
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) {
|
|
1286
|
+
matches.push(statement);
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
1290
|
+
continue;
|
|
1291
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1292
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name || declaration.initializer === undefined)
|
|
1293
|
+
continue;
|
|
1294
|
+
if (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) {
|
|
1295
|
+
matches.push(declaration.initializer);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return matches.length === 1 ? (matches[0] ?? null) : null;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Resolves a bounded JSON-like expression without executing consumer code.
|
|
1303
|
+
*
|
|
1304
|
+
* Args:
|
|
1305
|
+
* expression: source expression to resolve.
|
|
1306
|
+
* source: parsed file that may contain local consts and pure helpers.
|
|
1307
|
+
* bindings: literal arguments bound to a local helper's parameters.
|
|
1308
|
+
* depth: recursion depth used to reject cycles.
|
|
1309
|
+
*
|
|
1310
|
+
* Returns:
|
|
1311
|
+
* StaticJsonResult: a literal value when fully resolvable, else failure.
|
|
1312
|
+
*/
|
|
1313
|
+
function staticJsonValue(expression, source, bindings = new Map(), depth = 0) {
|
|
1314
|
+
if (depth > 8)
|
|
1315
|
+
return { ok: false };
|
|
1316
|
+
if (ts.isAsExpression(expression) ||
|
|
1317
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
1318
|
+
ts.isSatisfiesExpression(expression) ||
|
|
1319
|
+
ts.isNonNullExpression(expression)) {
|
|
1320
|
+
return staticJsonValue(expression.expression, source, bindings, depth + 1);
|
|
1321
|
+
}
|
|
1322
|
+
if (ts.isStringLiteralLike(expression))
|
|
1323
|
+
return { ok: true, value: expression.text };
|
|
1324
|
+
if (ts.isNumericLiteral(expression)) {
|
|
1325
|
+
const value = Number(expression.text);
|
|
1326
|
+
return Number.isFinite(value) ? { ok: true, value } : { ok: false };
|
|
1327
|
+
}
|
|
1328
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword)
|
|
1329
|
+
return { ok: true, value: true };
|
|
1330
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword)
|
|
1331
|
+
return { ok: true, value: false };
|
|
1332
|
+
if (expression.kind === ts.SyntaxKind.NullKeyword)
|
|
1333
|
+
return { ok: true, value: null };
|
|
1334
|
+
if (ts.isIdentifier(expression)) {
|
|
1335
|
+
const bound = bindings.get(expression.text);
|
|
1336
|
+
if (bound !== undefined)
|
|
1337
|
+
return { ok: true, value: bound };
|
|
1338
|
+
const initializer = localConstantInitializer(source, expression.text);
|
|
1339
|
+
return initializer === null ? { ok: false } : staticJsonValue(initializer, source, bindings, depth + 1);
|
|
1340
|
+
}
|
|
1341
|
+
if (ts.isArrayLiteralExpression(expression)) {
|
|
1342
|
+
const values = [];
|
|
1343
|
+
for (const element of expression.elements) {
|
|
1344
|
+
if (!ts.isExpression(element))
|
|
1345
|
+
return { ok: false };
|
|
1346
|
+
const resolved = staticJsonValue(element, source, bindings, depth + 1);
|
|
1347
|
+
if (!resolved.ok)
|
|
1348
|
+
return { ok: false };
|
|
1349
|
+
values.push(resolved.value);
|
|
1350
|
+
}
|
|
1351
|
+
return { ok: true, value: values };
|
|
1352
|
+
}
|
|
1353
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
1354
|
+
const value = {};
|
|
1355
|
+
for (const property of expression.properties) {
|
|
1356
|
+
if (!ts.isPropertyAssignment(property))
|
|
1357
|
+
return { ok: false };
|
|
1358
|
+
const key = property.name;
|
|
1359
|
+
if (!(ts.isIdentifier(key) || ts.isStringLiteralLike(key) || ts.isNumericLiteral(key)))
|
|
1360
|
+
return { ok: false };
|
|
1361
|
+
const resolved = staticJsonValue(property.initializer, source, bindings, depth + 1);
|
|
1362
|
+
if (!resolved.ok)
|
|
1363
|
+
return { ok: false };
|
|
1364
|
+
value[key.text] = resolved.value;
|
|
1365
|
+
}
|
|
1366
|
+
return { ok: true, value };
|
|
1367
|
+
}
|
|
1368
|
+
if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
1369
|
+
const helper = localStaticFunction(source, expression.expression.text);
|
|
1370
|
+
if (helper === null || helper.parameters.length !== expression.arguments.length)
|
|
1371
|
+
return { ok: false };
|
|
1372
|
+
const values = [];
|
|
1373
|
+
for (const argument of expression.arguments) {
|
|
1374
|
+
if (ts.isSpreadElement(argument))
|
|
1375
|
+
return { ok: false };
|
|
1376
|
+
const resolved = staticJsonValue(argument, source, bindings, depth + 1);
|
|
1377
|
+
if (!resolved.ok)
|
|
1378
|
+
return { ok: false };
|
|
1379
|
+
values.push(resolved.value);
|
|
1380
|
+
}
|
|
1381
|
+
const helperBindings = new Map();
|
|
1382
|
+
for (const [index, parameter] of helper.parameters.entries()) {
|
|
1383
|
+
if (!ts.isIdentifier(parameter.name))
|
|
1384
|
+
return { ok: false };
|
|
1385
|
+
const value = values[index];
|
|
1386
|
+
if (value === undefined)
|
|
1387
|
+
return { ok: false };
|
|
1388
|
+
helperBindings.set(parameter.name.text, value);
|
|
1389
|
+
}
|
|
1390
|
+
const helperBody = helper.body;
|
|
1391
|
+
let returned;
|
|
1392
|
+
if (helperBody === undefined)
|
|
1393
|
+
return { ok: false };
|
|
1394
|
+
if (!ts.isBlock(helperBody)) {
|
|
1395
|
+
returned = helperBody;
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
const statement = helperBody.statements[0];
|
|
1399
|
+
if (helperBody.statements.length === 1 &&
|
|
1400
|
+
statement !== undefined &&
|
|
1401
|
+
ts.isReturnStatement(statement) &&
|
|
1402
|
+
statement.expression !== undefined) {
|
|
1403
|
+
returned = statement.expression;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return returned === undefined
|
|
1407
|
+
? { ok: false }
|
|
1408
|
+
: staticJsonValue(returned, source, helperBindings, depth + 1);
|
|
1409
|
+
}
|
|
1410
|
+
return { ok: false };
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Extracts Gateforge claim annotations from one test call using literals only.
|
|
1414
|
+
*
|
|
1415
|
+
* Args:
|
|
1416
|
+
* node: parsed test-call node.
|
|
1417
|
+
* source: parsed test source file for local const/helper resolution.
|
|
1418
|
+
*
|
|
1419
|
+
* Returns:
|
|
1420
|
+
* { claims, issue }: deduplicated claim ids, or a visible reason when a
|
|
1421
|
+
* possible annotation cannot be resolved statically.
|
|
1422
|
+
*/
|
|
1423
|
+
function gateforgeAnnotationClaims(node, source) {
|
|
1424
|
+
if (!ts.isCallExpression(node))
|
|
1425
|
+
return { claims: [], issue: null };
|
|
1426
|
+
const options = node.arguments[1];
|
|
1427
|
+
if (options === undefined || ts.isArrowFunction(options) || ts.isFunctionExpression(options)) {
|
|
1428
|
+
return { claims: [], issue: null };
|
|
1429
|
+
}
|
|
1430
|
+
if (ts.isObjectLiteralExpression(options)) {
|
|
1431
|
+
const hasAnnotation = options.properties.some((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'annotation');
|
|
1432
|
+
if (!hasAnnotation) {
|
|
1433
|
+
return {
|
|
1434
|
+
claims: [],
|
|
1435
|
+
issue: options.properties.some(ts.isSpreadAssignment)
|
|
1436
|
+
? 'test options contain a spread that may hide a Gateforge annotation'
|
|
1437
|
+
: null,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
const resolved = staticJsonValue(options, source);
|
|
1442
|
+
if (!resolved.ok) {
|
|
1443
|
+
return { claims: [], issue: 'test options may contain a Gateforge annotation but are not a local literal or pure helper result' };
|
|
1444
|
+
}
|
|
1445
|
+
if (resolved.value === null || Array.isArray(resolved.value) || typeof resolved.value !== 'object') {
|
|
1446
|
+
return { claims: [], issue: null };
|
|
1447
|
+
}
|
|
1448
|
+
const annotation = resolved.value['annotation'];
|
|
1449
|
+
if (annotation === undefined)
|
|
1450
|
+
return { claims: [], issue: null };
|
|
1451
|
+
const items = Array.isArray(annotation) ? annotation : [annotation];
|
|
1452
|
+
const claims = [];
|
|
1453
|
+
for (const item of items) {
|
|
1454
|
+
if (item === null || Array.isArray(item) || typeof item !== 'object') {
|
|
1455
|
+
return { claims: [], issue: 'annotation value is not a statically resolved object' };
|
|
1456
|
+
}
|
|
1457
|
+
const type = item['type'];
|
|
1458
|
+
if (typeof type !== 'string') {
|
|
1459
|
+
return { claims: [], issue: 'annotation type is not a statically resolved string' };
|
|
1460
|
+
}
|
|
1461
|
+
if (type !== CLAIM_ANNOTATION_TYPE)
|
|
1462
|
+
continue;
|
|
1463
|
+
const description = item['description'];
|
|
1464
|
+
if (typeof description !== 'string' || description.length === 0) {
|
|
1465
|
+
return { claims: [], issue: 'Gateforge annotation description is not a non-empty static string' };
|
|
1466
|
+
}
|
|
1467
|
+
claims.push(description);
|
|
1468
|
+
}
|
|
1469
|
+
return { claims: [...new Set(claims)].sort(), issue: null };
|
|
1470
|
+
}
|
|
1037
1471
|
/** Builds one static entry: facts from the callback body + signals. */
|
|
1038
1472
|
function buildEntry(input) {
|
|
1039
1473
|
const { file, source, node, callback, titlePath, location } = input;
|
|
1474
|
+
const annotations = gateforgeAnnotationClaims(node, source);
|
|
1040
1475
|
const params = callback === undefined ? [] : signatureParamsOf(callback);
|
|
1041
1476
|
const signals = [...input.inheritedSignals];
|
|
1042
1477
|
for (const kind of input.suppression) {
|
|
@@ -1044,6 +1479,7 @@ function buildEntry(input) {
|
|
|
1044
1479
|
}
|
|
1045
1480
|
let pageRoute = null;
|
|
1046
1481
|
let httpClientCall = null;
|
|
1482
|
+
const interceptedTargets = callback === undefined ? [] : pageRouteTargets(callback);
|
|
1047
1483
|
if (callback !== undefined) {
|
|
1048
1484
|
pageRoute = findPageRoute(callback, source, file);
|
|
1049
1485
|
httpClientCall = findHttpClientCall(callback, source, file);
|
|
@@ -1058,11 +1494,14 @@ function buildEntry(input) {
|
|
|
1058
1494
|
facts: {
|
|
1059
1495
|
signatureParams: params,
|
|
1060
1496
|
pageRoute,
|
|
1497
|
+
...(interceptedTargets.length > 0 ? { pageRouteTargets: interceptedTargets } : {}),
|
|
1061
1498
|
httpClientCall,
|
|
1062
1499
|
fileHttpClientCall: input.fileHttpClient,
|
|
1063
1500
|
fileMockImport: input.fileMock,
|
|
1064
1501
|
gateforgeFixtureImport: input.gateforgeImport,
|
|
1065
1502
|
},
|
|
1503
|
+
...(annotations.claims.length > 0 ? { annotationClaims: annotations.claims } : {}),
|
|
1504
|
+
...(annotations.issue !== null ? { annotationIssue: annotations.issue } : {}),
|
|
1066
1505
|
};
|
|
1067
1506
|
}
|
|
1068
1507
|
//# sourceMappingURL=static-discovery.js.map
|