no-mistakes 0.86.3 → 0.87.3
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 +2 -0
- package/executed-postgres-sql-types.d.ts +39 -0
- package/index.d.ts +1 -1
- package/index.js +2 -0
- package/index.mjs +1 -0
- package/package.json +26 -7
- package/postgres-expression-types.d.ts +3 -2
- package/postgres-procedural-types.d.ts +4 -0
- package/postgres-query-types.d.ts +17 -0
- package/postgres-source-types.d.ts +1 -0
- package/test-timeout-policy/config.d.mts +11 -0
- package/test-timeout-policy/config.mjs +52 -0
- package/test-timeout-policy/index.d.mts +19 -0
- package/test-timeout-policy/index.mjs +129 -0
- package/test-timeout-policy/plugin.d.mts +19 -0
- package/test-timeout-policy/plugin.mjs +146 -0
- package/test-timeout-policy/public-runner.d.mts +13 -0
- package/test-timeout-policy/public-runner.mjs +118 -0
- package/test-timeout-policy/registration.d.mts +8 -0
- package/test-timeout-policy/registration.mjs +124 -0
package/README.md
CHANGED
|
@@ -193,6 +193,8 @@ npx no-mistakes rust-no-inline-tests crates/*/src
|
|
|
193
193
|
npx no-mistakes rust-max-lines-per-file crates/*/src crates/*/tests
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
The optional [Vitest timeout policy](../../docs/test-timeout-policy.md) provides public configuration and registration guards.
|
|
197
|
+
|
|
196
198
|
See the full documentation in [docs/](../../docs/README.md), the
|
|
197
199
|
[CLI command index](../../docs/cli/README.md), and the
|
|
198
200
|
[Node/N-API guide](../../docs/node-api.md). Agents can use the compact
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ExecutedPostgresSource {
|
|
2
|
+
fileName: string;
|
|
3
|
+
source: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ExecutedPostgresTrustedSqlTag {
|
|
7
|
+
module: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExecutedPostgresSqlOptions {
|
|
12
|
+
sources: ExecutedPostgresSource[];
|
|
13
|
+
/** Module whose named imports provide the configured executors. */
|
|
14
|
+
importSpecifier: string;
|
|
15
|
+
/** Defaults to query, read, write when importSpecifier is nonempty. */
|
|
16
|
+
executorNames?: string[];
|
|
17
|
+
executorFactoryNames?: string[];
|
|
18
|
+
executorTypeNames?: string[];
|
|
19
|
+
trustedSqlTags?: ExecutedPostgresTrustedSqlTag[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ExecutedPostgresSqlKind = "inline" | "immutableLocal" | "composed" | "dynamic";
|
|
23
|
+
|
|
24
|
+
export interface ExecutedPostgresSql {
|
|
25
|
+
fileName: string;
|
|
26
|
+
/** One-based line of the executor call. */
|
|
27
|
+
line: number;
|
|
28
|
+
annotation: string | null;
|
|
29
|
+
/** Interpolations are represented by sql_placeholder_N markers. */
|
|
30
|
+
sqlText: string | null;
|
|
31
|
+
/** False when full SQL could not be proven, including interpolation. */
|
|
32
|
+
complete: boolean;
|
|
33
|
+
kind: ExecutedPostgresSqlKind;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Extract configured PostgreSQL executor calls from supplied TS/JS source. */
|
|
37
|
+
export function extractExecutedPostgresSql(
|
|
38
|
+
options: ExecutedPostgresSqlOptions,
|
|
39
|
+
): Promise<ExecutedPostgresSql[]>;
|
package/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export function analyzeTypeScriptModules(
|
|
|
5
5
|
options: WithInvocationOptions<TypeScriptModulesOptions>,
|
|
6
6
|
): Promise<TypeScriptModulesReport>;
|
|
7
7
|
import type { PostgresSqlSource, PostgresSqlFacts } from "./postgres-source-types";
|
|
8
|
-
export
|
|
8
|
+
export * from "./postgres-source-types";
|
|
9
9
|
/** Parse SQL on the native async worker without repository I/O or a database. */
|
|
10
10
|
export function parsePostgresSql(source: PostgresSqlSource): Promise<PostgresSqlFacts>;
|
|
11
11
|
export function parsePostgresSql(sources: PostgresSqlSource[]): Promise<PostgresSqlFacts[]>;
|
package/index.js
CHANGED
|
@@ -26,6 +26,7 @@ function createJsonApis(descriptors) {
|
|
|
26
26
|
const jsonApis = createJsonApis({
|
|
27
27
|
analyzeTypeScriptModules: "analyzeTypeScriptModulesJson",
|
|
28
28
|
parsePostgresSql: "parsePostgresSqlJson",
|
|
29
|
+
extractExecutedPostgresSql: "extractExecutedPostgresSqlJson",
|
|
29
30
|
generatePostgresCatalog: "generatePostgresCatalogJson",
|
|
30
31
|
analyzeProject: "analyzeProjectJson",
|
|
31
32
|
callSites: "callSitesJson",
|
|
@@ -120,6 +121,7 @@ module.exports.createWorkflowTopologyIndex = createWorkflowTopologyIndex;
|
|
|
120
121
|
module.exports.version = version;
|
|
121
122
|
module.exports.analyzeProject = analyzeProject;
|
|
122
123
|
module.exports.analyzeTypeScriptModules = jsonApis.analyzeTypeScriptModules;
|
|
124
|
+
module.exports.extractExecutedPostgresSql = jsonApis.extractExecutedPostgresSql;
|
|
123
125
|
module.exports.writePlanningImpactArtifacts = writePlanningImpactArtifacts;
|
|
124
126
|
module.exports.callSites = jsonApis.callSites;
|
|
125
127
|
module.exports.check = jsonApis.check;
|
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "no-mistakes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.3",
|
|
4
4
|
"description": "Deterministic impact maps, test plans, Playwright coverage, and repository checks for coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
"workflow-topology-index-helpers.js",
|
|
30
30
|
"scripts/native-package.js",
|
|
31
31
|
"README.md",
|
|
32
|
-
"LICENSE"
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"test-timeout-policy/*.mjs",
|
|
34
|
+
"test-timeout-policy/*.d.mts",
|
|
35
|
+
"!test-timeout-policy/*.test.mjs"
|
|
33
36
|
],
|
|
34
37
|
"main": "index.js",
|
|
35
38
|
"types": "index.d.ts",
|
|
@@ -40,15 +43,31 @@
|
|
|
40
43
|
"require": "./index.js"
|
|
41
44
|
},
|
|
42
45
|
"./planning.js": "./planning.js",
|
|
43
|
-
"./workflow-topology-index.js": "./workflow-topology-index.js"
|
|
46
|
+
"./workflow-topology-index.js": "./workflow-topology-index.js",
|
|
47
|
+
"./test-timeout-policy": {
|
|
48
|
+
"types": "./test-timeout-policy/index.d.mts",
|
|
49
|
+
"import": "./test-timeout-policy/index.mjs"
|
|
50
|
+
},
|
|
51
|
+
"./test-timeout-policy/plugin": {
|
|
52
|
+
"types": "./test-timeout-policy/plugin.d.mts",
|
|
53
|
+
"import": "./test-timeout-policy/plugin.mjs"
|
|
54
|
+
}
|
|
44
55
|
},
|
|
45
56
|
"publishConfig": {
|
|
46
57
|
"access": "public"
|
|
47
58
|
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"vitest": "^5.0.3"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"vitest": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
48
67
|
"optionalDependencies": {
|
|
49
|
-
"no-mistakes-darwin-arm64": "0.
|
|
50
|
-
"no-mistakes-linux-arm64-gnu": "0.
|
|
51
|
-
"no-mistakes-linux-x64-gnu": "0.
|
|
52
|
-
"no-mistakes-win32-x64-msvc": "0.
|
|
68
|
+
"no-mistakes-darwin-arm64": "0.87.3",
|
|
69
|
+
"no-mistakes-linux-arm64-gnu": "0.87.3",
|
|
70
|
+
"no-mistakes-linux-x64-gnu": "0.87.3",
|
|
71
|
+
"no-mistakes-win32-x64-msvc": "0.87.3"
|
|
53
72
|
}
|
|
54
73
|
}
|
|
@@ -29,9 +29,10 @@ export type PostgresSqlExpressionRoot =
|
|
|
29
29
|
| { kind: "literal"; sql: string; value: PostgresSqlLiteralValue }
|
|
30
30
|
| { kind: "binary"; operator: string }
|
|
31
31
|
| { kind: "unary"; operator: string; expression: PostgresSqlExpressionRoot }
|
|
32
|
-
| { kind: "case" | "subquery" | "other" };
|
|
32
|
+
| { kind: "array" | "case" | "subquery" | "other" };
|
|
33
33
|
export type PostgresSqlExpressionChildRole =
|
|
34
34
|
| "argument"
|
|
35
|
+
| "arrayElement"
|
|
35
36
|
| "binaryLeft"
|
|
36
37
|
| "binaryRight"
|
|
37
38
|
| "caseOperand"
|
|
@@ -63,7 +64,7 @@ export type PostgresSqlExpressionChildRoot =
|
|
|
63
64
|
| { kind: "typedLiteral"; dataType: string; value: string; sql: string }
|
|
64
65
|
| { kind: "literal"; sql: string; value: PostgresSqlLiteralValue }
|
|
65
66
|
| { kind: "binary" | "unary"; operator: string }
|
|
66
|
-
| { kind: "case" | "subquery" | "other" };
|
|
67
|
+
| { kind: "array" | "case" | "subquery" | "other" };
|
|
67
68
|
export interface PostgresSqlExpressionChild {
|
|
68
69
|
role: PostgresSqlExpressionChildRole;
|
|
69
70
|
index: number | null;
|
|
@@ -17,6 +17,10 @@ export interface PostgresSqlProceduralBlock {
|
|
|
17
17
|
/** Classified source occurrences in source order. Visibility is not execution. */
|
|
18
18
|
occurrences: PostgresSqlProceduralOccurrence[];
|
|
19
19
|
diagnostics: PostgresSqlDiagnostic[];
|
|
20
|
+
/** True only when supported static SQL is reachable through typed statements.
|
|
21
|
+
* Unprojected loop utilities produce diagnostics and keep this false.
|
|
22
|
+
* Recognized utilities represented as `other` statements retain coverage.
|
|
23
|
+
* Occurrence classification is additive and never proves typed coverage. */
|
|
20
24
|
complete: boolean;
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PostgresSqlQueryStatement } from "./postgres-query-statement-types";
|
|
2
2
|
export type * from "./postgres-query-statement-types";
|
|
3
3
|
import type {
|
|
4
|
+
PostgresSqlExpression,
|
|
4
5
|
PostgresSqlIdentifier,
|
|
5
6
|
PostgresSqlName,
|
|
6
7
|
PostgresSqlSpan,
|
|
@@ -13,6 +14,7 @@ export interface PostgresSqlQuery {
|
|
|
13
14
|
columns: PostgresSqlQueryColumn[];
|
|
14
15
|
equalities: PostgresSqlQueryEquality[];
|
|
15
16
|
exists: PostgresSqlQueryExists[];
|
|
17
|
+
quantifiedComparisons: PostgresSqlQueryQuantifiedComparison[];
|
|
16
18
|
ctes: PostgresSqlQueryCte[];
|
|
17
19
|
/** Data-modifying query bodies in original source order; SELECT CTEs are excluded. */
|
|
18
20
|
nestedStatements: PostgresSqlQueryStatement[];
|
|
@@ -145,3 +147,18 @@ export interface PostgresSqlQueryUnsupported {
|
|
|
145
147
|
reason: string;
|
|
146
148
|
span: PostgresSqlSpan | null;
|
|
147
149
|
}
|
|
150
|
+
|
|
151
|
+
export type PostgresSqlQuantifier = "any" | "all";
|
|
152
|
+
/** Scalar array comparisons in traversal order; unsupported operands remain incomplete. */
|
|
153
|
+
export interface PostgresSqlQueryQuantifiedComparison {
|
|
154
|
+
scopeId: number;
|
|
155
|
+
clause: PostgresSqlQueryClause;
|
|
156
|
+
joinId: number | null;
|
|
157
|
+
operator: string;
|
|
158
|
+
quantifier: PostgresSqlQuantifier;
|
|
159
|
+
left: PostgresSqlExpression;
|
|
160
|
+
right: PostgresSqlExpression;
|
|
161
|
+
context: PostgresSqlPredicateContext;
|
|
162
|
+
span: PostgresSqlSpan | null;
|
|
163
|
+
complete: boolean;
|
|
164
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PostgresSqlTrigger } from "./postgres-trigger-types";
|
|
2
|
+
export * from "./executed-postgres-sql-types";
|
|
2
3
|
export type * from "./postgres-trigger-types";
|
|
3
4
|
import type { PostgresSqlWrapper } from "./postgres-wrapper-types";
|
|
4
5
|
export type * from "./postgres-wrapper-types";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function assertTestTimeout(value: unknown, label: string, maximum?: number): number;
|
|
2
|
+
export declare function assertTimeoutConfig(config: object, maximum?: number): void;
|
|
3
|
+
/** Preserve SDK option/property semantics while capturing one validated deadline. */
|
|
4
|
+
export declare function snapshotTimeoutOptions<T extends object>(
|
|
5
|
+
options: T,
|
|
6
|
+
label: string,
|
|
7
|
+
maximum?: number,
|
|
8
|
+
nullInherits?: boolean,
|
|
9
|
+
): T;
|
|
10
|
+
/** Install before collection, composing with the existing runner's config object. */
|
|
11
|
+
export declare function protectTimeoutConfig(config: object, maximum?: number): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Positive, finite configured deadlines; this does not cap aggregate run duration. */
|
|
2
|
+
const installedConfigs = new WeakMap();
|
|
3
|
+
export function assertTestTimeout(value, label, maximum = 30_000) {
|
|
4
|
+
if (typeof maximum !== "number" || !Number.isFinite(maximum) || maximum <= 0)
|
|
5
|
+
throw new RangeError("Timeout maximum must be positive and finite");
|
|
6
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > maximum)
|
|
7
|
+
throw new RangeError(`${label} must be positive, finite and at most ${maximum} ms`);
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function assertTimeoutConfig(config, maximum = 30_000) {
|
|
11
|
+
for (const key of ["testTimeout", "hookTimeout"]) {
|
|
12
|
+
if (Reflect.has(config, key)) assertTestTimeout(Reflect.get(config, key), key, maximum);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Preserve SDK option/property semantics while capturing one validated deadline. */
|
|
16
|
+
export function snapshotTimeoutOptions(options, label, maximum = 30_000, nullInherits = false) {
|
|
17
|
+
// Vitest copies registration options with object spread, ignoring inherited/hidden keys.
|
|
18
|
+
if (!Object.prototype.propertyIsEnumerable.call(options, "timeout")) return options;
|
|
19
|
+
const timeout = Reflect.get(options, "timeout");
|
|
20
|
+
if (timeout !== undefined && (timeout !== null || !nullInherits))
|
|
21
|
+
assertTestTimeout(timeout, label, maximum);
|
|
22
|
+
return new Proxy(options, {
|
|
23
|
+
get(target, key) {
|
|
24
|
+
return key === "timeout" ? timeout : Reflect.get(target, key, target);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Install before collection, composing with the existing runner's config object. */
|
|
29
|
+
export function protectTimeoutConfig(config, maximum = 30_000) {
|
|
30
|
+
const existing = installedConfigs.get(config);
|
|
31
|
+
if (existing !== undefined) {
|
|
32
|
+
if (existing !== maximum) throw new Error("Timeout configuration policy maximum cannot change");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const values = ["testTimeout", "hookTimeout"].map((key) => ({
|
|
36
|
+
key,
|
|
37
|
+
value: assertTestTimeout(Reflect.get(config, key), key, maximum),
|
|
38
|
+
}));
|
|
39
|
+
for (const entry of values) {
|
|
40
|
+
const key = entry.key;
|
|
41
|
+
let value = entry.value;
|
|
42
|
+
Object.defineProperty(config, key, {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: false,
|
|
45
|
+
get: () => value,
|
|
46
|
+
set: (next) => {
|
|
47
|
+
value = assertTestTimeout(next, key, maximum);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
installedConfigs.set(config, maximum);
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { guardRegistration, type Registration, type Kind } from "./registration.mjs";
|
|
2
|
+
export { guardPublicRunner, type RunnerConstructor, type Protector } from "./public-runner.mjs";
|
|
3
|
+
export {
|
|
4
|
+
assertTestTimeout,
|
|
5
|
+
assertTimeoutConfig,
|
|
6
|
+
snapshotTimeoutOptions,
|
|
7
|
+
protectTimeoutConfig,
|
|
8
|
+
} from "./config.mjs";
|
|
9
|
+
/** Aliased/destructured setters retain validation because the returned method is guarded. */
|
|
10
|
+
export declare function guardRuntimeConfig<T extends object>(api: T, maximum?: number): T;
|
|
11
|
+
/** A single current Vitest API facade. Adoption must route every test import through it. */
|
|
12
|
+
export declare function guardVitestExports<T extends object>(api: T, maximum?: number): T;
|
|
13
|
+
/** Compose at runner construction, without replacing existing collection/database guards. */
|
|
14
|
+
export declare function protectRunnerTimeouts(
|
|
15
|
+
runner: {
|
|
16
|
+
config: object;
|
|
17
|
+
},
|
|
18
|
+
maximum?: number,
|
|
19
|
+
): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { guardRegistration } from "./registration.mjs";
|
|
2
|
+
export { guardRegistration } from "./registration.mjs";
|
|
3
|
+
import { guardCollectionRunner } from "./public-runner.mjs";
|
|
4
|
+
export { guardPublicRunner } from "./public-runner.mjs";
|
|
5
|
+
import { assertTestTimeout, assertTimeoutConfig, protectTimeoutConfig } from "./config.mjs";
|
|
6
|
+
export {
|
|
7
|
+
assertTestTimeout,
|
|
8
|
+
assertTimeoutConfig,
|
|
9
|
+
snapshotTimeoutOptions,
|
|
10
|
+
protectTimeoutConfig,
|
|
11
|
+
} from "./config.mjs";
|
|
12
|
+
const installedRunners = new WeakMap();
|
|
13
|
+
const installedContexts = new WeakMap();
|
|
14
|
+
function protectContextHooks(context, maximum) {
|
|
15
|
+
const installed = installedContexts.get(context) ?? { maximum, methods: new Set() };
|
|
16
|
+
if (installed.maximum !== maximum)
|
|
17
|
+
throw new Error("Context timeout policy maximum cannot change");
|
|
18
|
+
for (const name of ["onTestFailed", "onTestFinished"]) {
|
|
19
|
+
// Extensions may add a missing hook to the same context between scans.
|
|
20
|
+
if (installed.methods.has(name)) continue;
|
|
21
|
+
const original = Reflect.get(context, name);
|
|
22
|
+
if (typeof original === "function") {
|
|
23
|
+
let guarded = guardRegistration(original, "hook", maximum);
|
|
24
|
+
Object.defineProperty(context, name, {
|
|
25
|
+
// Vitest temporarily replaces/restores these methods while executing completion hooks.
|
|
26
|
+
get: () => guarded,
|
|
27
|
+
set(next) {
|
|
28
|
+
if (typeof next !== "function") throw new TypeError("Expected a completion hook method");
|
|
29
|
+
guarded = guardRegistration(next, "hook", maximum);
|
|
30
|
+
},
|
|
31
|
+
configurable: false,
|
|
32
|
+
});
|
|
33
|
+
installed.methods.add(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
installedContexts.set(context, installed);
|
|
37
|
+
}
|
|
38
|
+
/** Aliased/destructured setters retain validation because the returned method is guarded. */
|
|
39
|
+
export function guardRuntimeConfig(api, maximum = 30_000) {
|
|
40
|
+
const original = Reflect.get(api, "setConfig");
|
|
41
|
+
if (typeof original !== "function") throw new TypeError("Expected a Vitest setConfig method");
|
|
42
|
+
const setter = (config) => {
|
|
43
|
+
const snapshot = { ...config };
|
|
44
|
+
assertTimeoutConfig(snapshot, maximum);
|
|
45
|
+
return Reflect.apply(original, api, [snapshot]);
|
|
46
|
+
};
|
|
47
|
+
return new Proxy(api, {
|
|
48
|
+
get(target, key, receiver) {
|
|
49
|
+
return key === "setConfig" ? setter : Reflect.get(target, key, receiver);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** A single current Vitest API facade. Adoption must route every test import through it. */
|
|
54
|
+
export function guardVitestExports(api, maximum = 30_000) {
|
|
55
|
+
const cache = new Map();
|
|
56
|
+
const kinds = new Map([
|
|
57
|
+
["test", "test"],
|
|
58
|
+
["it", "test"],
|
|
59
|
+
["describe", "suite"],
|
|
60
|
+
["suite", "suite"],
|
|
61
|
+
["beforeAll", "hook"],
|
|
62
|
+
["beforeEach", "hook"],
|
|
63
|
+
["afterAll", "hook"],
|
|
64
|
+
["afterEach", "hook"],
|
|
65
|
+
["aroundAll", "hook"],
|
|
66
|
+
["aroundEach", "hook"],
|
|
67
|
+
["onTestFailed", "hook"],
|
|
68
|
+
["onTestFinished", "hook"],
|
|
69
|
+
]);
|
|
70
|
+
return new Proxy(api, {
|
|
71
|
+
get(target, key, receiver) {
|
|
72
|
+
const value = Reflect.get(target, key, receiver);
|
|
73
|
+
const kind = kinds.get(String(key));
|
|
74
|
+
const role = kind ?? (key === "vitest" ? "vi" : key);
|
|
75
|
+
const values = cache.get(value) ?? new Map();
|
|
76
|
+
if (values.has(role)) return values.get(role);
|
|
77
|
+
const guarded =
|
|
78
|
+
key === "TestRunner" && typeof value === "function"
|
|
79
|
+
? guardCollectionRunner(value, maximum, protectRunnerTimeouts)
|
|
80
|
+
: (key === "vi" || key === "vitest") && value !== null && typeof value === "object"
|
|
81
|
+
? guardRuntimeConfig(value, maximum)
|
|
82
|
+
: kind && typeof value === "function"
|
|
83
|
+
? guardRegistration(value, kind, maximum)
|
|
84
|
+
: value;
|
|
85
|
+
values.set(role, guarded);
|
|
86
|
+
cache.set(value, values);
|
|
87
|
+
return guarded;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Compose at runner construction, without replacing existing collection/database guards. */
|
|
92
|
+
export function protectRunnerTimeouts(runner, maximum = 30_000) {
|
|
93
|
+
const existing = installedRunners.get(runner);
|
|
94
|
+
if (existing) {
|
|
95
|
+
if (existing.maximum !== maximum || existing.config !== runner.config)
|
|
96
|
+
throw new Error("Installed runner timeout policy cannot be replaced");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (Reflect.get(runner.config, "globals") === true)
|
|
100
|
+
throw new Error("Timeout policy requires guarded public imports with globals disabled");
|
|
101
|
+
protectTimeoutConfig(runner.config, maximum);
|
|
102
|
+
const config = runner.config;
|
|
103
|
+
const extend = Reflect.get(runner, "extendTaskContext");
|
|
104
|
+
const extendTaskContext = (context) => {
|
|
105
|
+
if (runner.config !== config)
|
|
106
|
+
throw new Error("Runner timeout configuration identity changed after construction");
|
|
107
|
+
// Vitest 5 creates the context after resolving task.timeout, before withTimeout.
|
|
108
|
+
// withTimeout uses a pre-context local timeout; extensions must not mask invalid tasks.
|
|
109
|
+
const task = Reflect.get(context, "task");
|
|
110
|
+
if (task === null || typeof task !== "object")
|
|
111
|
+
throw new TypeError("Expected Vitest task context");
|
|
112
|
+
assertTestTimeout(Reflect.get(task, "timeout"), "Vitest resolved test timeout", maximum);
|
|
113
|
+
protectContextHooks(context, maximum);
|
|
114
|
+
const result =
|
|
115
|
+
typeof extend === "function" ? Reflect.apply(extend, runner, [context]) : context;
|
|
116
|
+
if (runner.config !== config)
|
|
117
|
+
throw new Error("Runner timeout configuration identity changed during context extension");
|
|
118
|
+
// Recheck the original task, even if the extension returns a replacement context.
|
|
119
|
+
assertTestTimeout(Reflect.get(task, "timeout"), "Vitest resolved test timeout", maximum);
|
|
120
|
+
protectContextHooks(result, maximum);
|
|
121
|
+
return result;
|
|
122
|
+
};
|
|
123
|
+
Object.defineProperty(runner, "extendTaskContext", {
|
|
124
|
+
value: extendTaskContext,
|
|
125
|
+
writable: false,
|
|
126
|
+
configurable: false,
|
|
127
|
+
});
|
|
128
|
+
installedRunners.set(runner, { maximum, config: runner.config });
|
|
129
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Plugin } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export interface NodeRunnerPolicy {
|
|
4
|
+
/** Absolute final custom runner module, or null for Vitest's native runner. */
|
|
5
|
+
runner: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Route Node registration imports and protect the final native or custom runner. */
|
|
9
|
+
export declare function testTimeoutPolicyPlugin(
|
|
10
|
+
maximum?: number,
|
|
11
|
+
registrationPackages?: readonly string[],
|
|
12
|
+
nodeRunner?: NodeRunnerPolicy,
|
|
13
|
+
): Plugin;
|
|
14
|
+
|
|
15
|
+
/** Route the native Vitest Browser Mode runner and registrar dependency closure. */
|
|
16
|
+
export declare function browserTestTimeoutPolicyPlugin(
|
|
17
|
+
maximum?: number,
|
|
18
|
+
registrationPackages?: readonly string[],
|
|
19
|
+
): Plugin;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { configDefaults } from "vitest/config";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const virtualId = "\0test-timeout-policy:vitest";
|
|
5
|
+
/** Vite resolves the public imports; no source parser or SDK-private patch is involved. */
|
|
6
|
+
export function testTimeoutPolicyPlugin(
|
|
7
|
+
maximum = 30_000,
|
|
8
|
+
registrationPackages = [],
|
|
9
|
+
nodeRunner = { runner: null },
|
|
10
|
+
) {
|
|
11
|
+
return createPolicyPlugin(maximum, registrationPackages, nodeRunner);
|
|
12
|
+
}
|
|
13
|
+
function createPolicyPlugin(maximum, registrationPackages, nodeRunner) {
|
|
14
|
+
if (!Number.isFinite(maximum) || maximum <= 0)
|
|
15
|
+
throw new RangeError("Timeout maximum must be positive and finite");
|
|
16
|
+
const policy = fileURLToPath(new URL("./index.mjs", import.meta.url));
|
|
17
|
+
const runnerId = fileURLToPath(new URL("./timeout-policy-virtual-runner.mjs", import.meta.url));
|
|
18
|
+
const originalRunner = nodeRunner?.runner;
|
|
19
|
+
if (originalRunner && !isAbsolute(originalRunner))
|
|
20
|
+
throw new TypeError("Supply the absolute resolved final runner module");
|
|
21
|
+
return {
|
|
22
|
+
name: "test-timeout-policy",
|
|
23
|
+
enforce: "pre",
|
|
24
|
+
// Externalized modules bypass Vite resolution entirely; registration helpers must be inlined.
|
|
25
|
+
config(config = {}) {
|
|
26
|
+
if (nodeRunner && config.test?.runner && config.test.runner !== originalRunner)
|
|
27
|
+
throw new Error("Configured runner differs from the supplied final runner");
|
|
28
|
+
return {
|
|
29
|
+
test: {
|
|
30
|
+
...(nodeRunner ? { runner: runnerId } : {}),
|
|
31
|
+
server: {
|
|
32
|
+
deps: {
|
|
33
|
+
inline: config.test?.server?.deps?.inline === true ? true : [...registrationPackages],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
configResolved(config) {
|
|
40
|
+
if (config.test?.browser?.enabled)
|
|
41
|
+
throw new Error("Node final-runner composition cannot be used for browser projects");
|
|
42
|
+
if (config.test?.runner !== runnerId)
|
|
43
|
+
throw new Error("Final runner policy was replaced during configuration merge");
|
|
44
|
+
},
|
|
45
|
+
resolveId(source, importer) {
|
|
46
|
+
if (nodeRunner && source === runnerId) return runnerId;
|
|
47
|
+
if (source === "vitest" && importer === runnerId) return null;
|
|
48
|
+
if (source !== "vitest" || !importer || importer === virtualId) return null;
|
|
49
|
+
return virtualId;
|
|
50
|
+
},
|
|
51
|
+
load(id) {
|
|
52
|
+
if (nodeRunner && id === runnerId)
|
|
53
|
+
return `
|
|
54
|
+
${
|
|
55
|
+
originalRunner
|
|
56
|
+
? `import FinalRunner from ${JSON.stringify(originalRunner)}`
|
|
57
|
+
: "import { TestRunner as FinalRunner } from 'vitest'"
|
|
58
|
+
}
|
|
59
|
+
import { guardPublicRunner, protectRunnerTimeouts } from ${JSON.stringify(policy)}
|
|
60
|
+
export default guardPublicRunner(FinalRunner, ${JSON.stringify(maximum)}, protectRunnerTimeouts)
|
|
61
|
+
`;
|
|
62
|
+
if (id !== virtualId) return null;
|
|
63
|
+
return `
|
|
64
|
+
import * as native from 'vitest'
|
|
65
|
+
import { guardVitestExports } from ${JSON.stringify(policy)}
|
|
66
|
+
${
|
|
67
|
+
nodeRunner
|
|
68
|
+
? `import { guardRunnerStatics } from ${JSON.stringify(fileURLToPath(new URL("./public-runner.mjs", import.meta.url)))}
|
|
69
|
+
import { protectRunnerTimeouts } from ${JSON.stringify(policy)}`
|
|
70
|
+
: ""
|
|
71
|
+
}
|
|
72
|
+
export * from 'vitest'
|
|
73
|
+
const guarded = guardVitestExports(native, ${JSON.stringify(maximum)})
|
|
74
|
+
${nodeRunner ? `export const TestRunner = guardRunnerStatics(native.TestRunner, ${JSON.stringify(maximum)}, protectRunnerTimeouts)` : "export const TestRunner = guarded.TestRunner"}
|
|
75
|
+
export const { test, it, describe, suite, beforeAll, beforeEach, afterAll, afterEach,
|
|
76
|
+
aroundAll, aroundEach, onTestFailed, onTestFinished, vi, vitest } = guarded
|
|
77
|
+
`;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Browser-only candidate: keep public constructor/registrars out of dependency bundles. */
|
|
82
|
+
export function browserTestTimeoutPolicyPlugin(maximum = 30_000, registrationPackages = []) {
|
|
83
|
+
const base = createPolicyPlugin(maximum, registrationPackages);
|
|
84
|
+
const excluded = ["vitest", "@vitest/browser", ...registrationPackages];
|
|
85
|
+
const filterIncludes = (include) =>
|
|
86
|
+
include.filter(
|
|
87
|
+
(entry) =>
|
|
88
|
+
!entry.split(">").some((part) => {
|
|
89
|
+
const token = part.trim();
|
|
90
|
+
return excluded.some((id) => token === id || token.startsWith(`${id}/`));
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
return {
|
|
94
|
+
...base,
|
|
95
|
+
name: "browser-test-timeout-policy",
|
|
96
|
+
configEnvironment: {
|
|
97
|
+
order: "post",
|
|
98
|
+
handler(name, config) {
|
|
99
|
+
if (name !== "client" && config.consumer !== "client") return;
|
|
100
|
+
return {
|
|
101
|
+
optimizeDeps: {
|
|
102
|
+
...config.optimizeDeps,
|
|
103
|
+
include: filterIncludes(config.optimizeDeps?.include ?? []),
|
|
104
|
+
exclude: [...new Set([...(config.optimizeDeps?.exclude ?? []), ...excluded])],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
config(config = {}) {
|
|
110
|
+
const existing = config.test?.reporters;
|
|
111
|
+
const reporters = existing === undefined ? [...configDefaults.reporters] : [];
|
|
112
|
+
const lifecycle = {
|
|
113
|
+
onBrowserInit(project) {
|
|
114
|
+
// Public reporter lifecycle runs before the provider starts browser pages.
|
|
115
|
+
const configs = [project.viteConfig, project.vite.config];
|
|
116
|
+
for (const resolved of configs) {
|
|
117
|
+
for (const optimizer of [
|
|
118
|
+
resolved.optimizeDeps,
|
|
119
|
+
resolved.environments?.client?.optimizeDeps,
|
|
120
|
+
]) {
|
|
121
|
+
if (optimizer?.include) optimizer.include = filterIncludes(optimizer.include);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const optimizer = project.vite.environments.client?.config.optimizeDeps;
|
|
125
|
+
if (optimizer?.include) optimizer.include = filterIncludes(optimizer.include);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
...base.config(config),
|
|
130
|
+
test: { ...base.config(config).test, reporters: [...reporters, lifecycle] },
|
|
131
|
+
optimizeDeps: { exclude: excluded },
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
configResolved(config) {
|
|
135
|
+
if (!config.test?.browser?.enabled)
|
|
136
|
+
throw new Error("Browser timeout policy requires an enabled browser project");
|
|
137
|
+
for (const id of excluded) {
|
|
138
|
+
if (!config.optimizeDeps?.exclude?.includes(id))
|
|
139
|
+
throw new Error(`Missing browser timeout routing exclusion: ${id}`);
|
|
140
|
+
}
|
|
141
|
+
for (const optimizer of [config.optimizeDeps, config.environments?.client?.optimizeDeps]) {
|
|
142
|
+
if (optimizer?.include) optimizer.include = filterIncludes(optimizer.include);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Protector<T extends { config: object } = { config: object }> = (
|
|
2
|
+
runner: T,
|
|
3
|
+
maximum: number,
|
|
4
|
+
) => void;
|
|
5
|
+
export type RunnerConstructor = new (...args: never[]) => {
|
|
6
|
+
config: object;
|
|
7
|
+
};
|
|
8
|
+
/** Compose a final public constructor while retaining direct and derived new.target contracts. */
|
|
9
|
+
export declare function guardPublicRunner<T extends RunnerConstructor>(
|
|
10
|
+
original: T,
|
|
11
|
+
maximum: number,
|
|
12
|
+
protect: Protector<InstanceType<T>>,
|
|
13
|
+
): T;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { guardRegistration, protectSuiteCollector } from "./registration.mjs";
|
|
2
|
+
const constructors = new WeakMap();
|
|
3
|
+
/** Compose a final public constructor while retaining direct and derived new.target contracts. */
|
|
4
|
+
export function guardPublicRunner(original, maximum, protect) {
|
|
5
|
+
return guardRunner(original, maximum, protect, false);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Internal adapter for the native SDK browser subclass, whose collection lifecycle is public. */
|
|
9
|
+
export function guardCollectionRunner(original, maximum, protect) {
|
|
10
|
+
return guardRunner(original, maximum, protect, true);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Node final-runner composition owns instance protection; public statics still need guards. */
|
|
14
|
+
export function guardRunnerStatics(original, maximum, protect) {
|
|
15
|
+
return guardRunner(original, maximum, protect, null);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function guardRunner(original, maximum, protect, defer) {
|
|
19
|
+
let policies = constructors.get(original);
|
|
20
|
+
if (!policies) {
|
|
21
|
+
policies = new WeakMap();
|
|
22
|
+
constructors.set(original, policies);
|
|
23
|
+
}
|
|
24
|
+
let deadlines = policies.get(protect);
|
|
25
|
+
if (!deadlines) {
|
|
26
|
+
deadlines = new Map();
|
|
27
|
+
policies.set(protect, deadlines);
|
|
28
|
+
}
|
|
29
|
+
let adapters = deadlines.get(maximum);
|
|
30
|
+
if (!adapters) {
|
|
31
|
+
adapters = new Map();
|
|
32
|
+
deadlines.set(maximum, adapters);
|
|
33
|
+
}
|
|
34
|
+
const existing = adapters.get(defer);
|
|
35
|
+
if (existing) return existing;
|
|
36
|
+
const methods = new WeakMap();
|
|
37
|
+
const statics = new Map();
|
|
38
|
+
const guarded = new Proxy(original, {
|
|
39
|
+
get(target, key, receiver) {
|
|
40
|
+
const direct = receiver === guarded;
|
|
41
|
+
const value = Reflect.get(target, key, direct ? target : receiver);
|
|
42
|
+
if (
|
|
43
|
+
typeof value === "function" &&
|
|
44
|
+
["getCurrentSuite", "createTaskCollector", "getSuiteHooks", "setSuiteHooks"].includes(key)
|
|
45
|
+
) {
|
|
46
|
+
if (!statics.has(value)) {
|
|
47
|
+
statics.set(
|
|
48
|
+
value,
|
|
49
|
+
new Proxy(value, {
|
|
50
|
+
apply(callable, owner, args) {
|
|
51
|
+
if (key === "getSuiteHooks" || key === "setSuiteHooks")
|
|
52
|
+
throw new Error(
|
|
53
|
+
"TestRunner raw suite hooks bypass native hook deadlines; register guarded public hooks instead",
|
|
54
|
+
);
|
|
55
|
+
const result = Reflect.apply(callable, owner === guarded ? target : owner, args);
|
|
56
|
+
return key === "getCurrentSuite"
|
|
57
|
+
? protectSuiteCollector(result, maximum)
|
|
58
|
+
: guardRegistration(result, "test", maximum);
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return statics.get(value);
|
|
64
|
+
}
|
|
65
|
+
if (!direct || typeof value !== "function") return value;
|
|
66
|
+
const descriptor = staticDescriptor(target, key);
|
|
67
|
+
// Callable-valued fields are public values, including Vitest's exported static aliases.
|
|
68
|
+
if (
|
|
69
|
+
!descriptor ||
|
|
70
|
+
typeof descriptor.value !== "function" ||
|
|
71
|
+
descriptor.enumerable === true ||
|
|
72
|
+
(descriptor?.configurable === false && descriptor.writable === false)
|
|
73
|
+
)
|
|
74
|
+
return value;
|
|
75
|
+
const existing = methods.get(value);
|
|
76
|
+
if (existing) return existing;
|
|
77
|
+
const method = new Proxy(value, {
|
|
78
|
+
apply(callable, owner, args) {
|
|
79
|
+
return Reflect.apply(callable, owner === guarded ? target : owner, args);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
methods.set(value, method);
|
|
83
|
+
return method;
|
|
84
|
+
},
|
|
85
|
+
construct(target, args, newTarget) {
|
|
86
|
+
const runner = Reflect.construct(target, args, newTarget === guarded ? target : newTarget);
|
|
87
|
+
if (newTarget === guarded && defer !== null) protect(runner, maximum);
|
|
88
|
+
else if (defer) {
|
|
89
|
+
// super() returns before derived fields initialize. Vitest's public
|
|
90
|
+
// collection lifecycle runs after construction, before registration.
|
|
91
|
+
const beforeCollect = Reflect.get(runner, "onBeforeCollect");
|
|
92
|
+
Object.defineProperty(runner, "onBeforeCollect", {
|
|
93
|
+
configurable: false,
|
|
94
|
+
writable: false,
|
|
95
|
+
value(...paths) {
|
|
96
|
+
protect(runner, maximum);
|
|
97
|
+
return typeof beforeCollect === "function"
|
|
98
|
+
? Reflect.apply(beforeCollect, runner, paths)
|
|
99
|
+
: undefined;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return runner;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
adapters.set(defer, guarded);
|
|
107
|
+
return guarded;
|
|
108
|
+
}
|
|
109
|
+
/** Class-owned descriptors distinguish methods from fields without changing Function APIs. */
|
|
110
|
+
function staticDescriptor(target, key) {
|
|
111
|
+
let owner = target;
|
|
112
|
+
while (owner && owner !== Function.prototype) {
|
|
113
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(owner, key);
|
|
114
|
+
if (descriptor) return descriptor;
|
|
115
|
+
owner = Reflect.getPrototypeOf(owner);
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type Registration = (...args: never[]) => unknown;
|
|
2
|
+
export type Kind = "test" | "suite" | "hook";
|
|
3
|
+
/** Validate before Vitest's registration creates its captured timeout wrapper. */
|
|
4
|
+
export declare function guardRegistration<T extends Registration>(
|
|
5
|
+
original: T,
|
|
6
|
+
kind: Kind,
|
|
7
|
+
maximum?: number,
|
|
8
|
+
): T;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { assertTestTimeout, snapshotTimeoutOptions } from "./config.mjs";
|
|
2
|
+
const registrations = new WeakMap();
|
|
3
|
+
const curriedMethods = new Set(["each", "for", "extend", "override", "scoped", "skipIf", "runIf"]);
|
|
4
|
+
const chainedMethods = new Set(["concurrent", "shuffle", "skip", "only", "todo", "fails"]);
|
|
5
|
+
const suiteMethods = new Set(["describe", "suite"]);
|
|
6
|
+
const hookMethods = new Set([
|
|
7
|
+
"beforeAll",
|
|
8
|
+
"beforeEach",
|
|
9
|
+
"afterAll",
|
|
10
|
+
"afterEach",
|
|
11
|
+
"aroundAll",
|
|
12
|
+
"aroundEach",
|
|
13
|
+
]);
|
|
14
|
+
/** Validate before Vitest's registration creates its captured timeout wrapper. */
|
|
15
|
+
export function guardRegistration(original, kind, maximum = 30_000) {
|
|
16
|
+
return createRegistration(original, kind, maximum, true);
|
|
17
|
+
}
|
|
18
|
+
function createRegistration(original, kind, maximum, injectFactory) {
|
|
19
|
+
const deadlines = registrations.get(original) ?? new Map();
|
|
20
|
+
const roles = deadlines.get(maximum) ?? new Map();
|
|
21
|
+
const role = `${kind}:${injectFactory}`;
|
|
22
|
+
const existing = roles.get(role);
|
|
23
|
+
if (existing) return existing;
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
const guarded = new Proxy(original, {
|
|
26
|
+
apply(target, receiver, args) {
|
|
27
|
+
if (kind === "hook") {
|
|
28
|
+
if (args[1] !== undefined) assertTestTimeout(args[1], "hook timeout", maximum);
|
|
29
|
+
} else {
|
|
30
|
+
for (let index = 1; index < Math.min(args.length, 3); index++) {
|
|
31
|
+
const value = args[index];
|
|
32
|
+
if (kind === "suite" && injectFactory && typeof value === "function") {
|
|
33
|
+
args[index] = protectSuiteFactory(value, maximum);
|
|
34
|
+
} else if (typeof value === "number")
|
|
35
|
+
assertTestTimeout(value, `${kind} timeout`, maximum);
|
|
36
|
+
else if (value !== null && typeof value === "object") {
|
|
37
|
+
args[index] = snapshotTimeoutOptions(value, `${kind} timeout`, maximum, true);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const result = Reflect.apply(target, receiver, args);
|
|
42
|
+
return kind === "suite" &&
|
|
43
|
+
result !== null &&
|
|
44
|
+
typeof result === "object" &&
|
|
45
|
+
Reflect.get(result, "type") === "collector"
|
|
46
|
+
? protectSuiteCollector(result, maximum)
|
|
47
|
+
: result;
|
|
48
|
+
},
|
|
49
|
+
get(target, key, receiver) {
|
|
50
|
+
if (cache.has(key)) return cache.get(key);
|
|
51
|
+
const value = Reflect.get(target, key, receiver);
|
|
52
|
+
if (typeof value !== "function") return value;
|
|
53
|
+
const name = String(key);
|
|
54
|
+
if (
|
|
55
|
+
!curriedMethods.has(name) &&
|
|
56
|
+
!chainedMethods.has(name) &&
|
|
57
|
+
!hookMethods.has(name) &&
|
|
58
|
+
!suiteMethods.has(name)
|
|
59
|
+
)
|
|
60
|
+
return value;
|
|
61
|
+
const guarded = curriedMethods.has(name)
|
|
62
|
+
? new Proxy(value, {
|
|
63
|
+
apply(method, owner, args) {
|
|
64
|
+
const result = Reflect.apply(method, owner, args);
|
|
65
|
+
return typeof result === "function"
|
|
66
|
+
? createRegistration(
|
|
67
|
+
result,
|
|
68
|
+
kind,
|
|
69
|
+
maximum,
|
|
70
|
+
injectFactory && name !== "each" && name !== "for",
|
|
71
|
+
)
|
|
72
|
+
: result;
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
: guardRegistration(
|
|
76
|
+
value,
|
|
77
|
+
hookMethods.has(name) ? "hook" : suiteMethods.has(name) ? "suite" : kind,
|
|
78
|
+
maximum,
|
|
79
|
+
);
|
|
80
|
+
cache.set(key, guarded);
|
|
81
|
+
return guarded;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
roles.set(role, guarded);
|
|
85
|
+
deadlines.set(maximum, roles);
|
|
86
|
+
registrations.set(original, deadlines);
|
|
87
|
+
// Native completion-hook execution saves and restores a method that is already guarded.
|
|
88
|
+
registrations.set(guarded, new Map([[maximum, new Map([[role, guarded]])]]));
|
|
89
|
+
return guarded;
|
|
90
|
+
}
|
|
91
|
+
function protectSuiteFactory(factory, maximum) {
|
|
92
|
+
return new Proxy(factory, {
|
|
93
|
+
apply(target, owner, values) {
|
|
94
|
+
// The known direct suite-factory route always receives Vitest's public TestAPI.
|
|
95
|
+
values[0] = guardRegistration(values[0], "test", maximum);
|
|
96
|
+
return Reflect.apply(target, owner, values);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const collectors = new WeakMap();
|
|
101
|
+
/** Native collector.on stores raw callbacks, bypassing Vitest's public hook timeout capture. */
|
|
102
|
+
export function protectSuiteCollector(original, maximum) {
|
|
103
|
+
const deadlines = collectors.get(original) ?? new Map();
|
|
104
|
+
const existing = deadlines.get(maximum);
|
|
105
|
+
if (existing) return existing;
|
|
106
|
+
const tests = new Map();
|
|
107
|
+
const rejectRawHook = () => {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"SuiteCollector.on bypasses native hook deadlines; register guarded public hooks inside the suite factory",
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
const guarded = new Proxy(original, {
|
|
113
|
+
get(target, key, receiver) {
|
|
114
|
+
if (key === "on") return rejectRawHook;
|
|
115
|
+
const value = Reflect.get(target, key, receiver);
|
|
116
|
+
if (key !== "test" || typeof value !== "function") return value;
|
|
117
|
+
if (!tests.has(value)) tests.set(value, guardRegistration(value, "test", maximum));
|
|
118
|
+
return tests.get(value);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
deadlines.set(maximum, guarded);
|
|
122
|
+
collectors.set(original, deadlines);
|
|
123
|
+
return guarded;
|
|
124
|
+
}
|