@jterrazz/test 12.1.0 → 13.0.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 +16 -2
- package/dist/checker.js +587 -15
- package/dist/index.d.ts +194 -17
- package/dist/index.js +66 -1145
- package/dist/literate.js +1726 -0
- package/dist/match.js +73 -1
- package/dist/oxlint.cjs +168 -13
- package/dist/oxlint.js +168 -13
- package/dist/queue.js +3 -295
- package/dist/spec-document.js +536 -0
- package/dist/vitest.d.ts +41 -0
- package/dist/vitest.js +69 -0
- package/package.json +10 -4
- package/schema/spec.schema.json +179 -0
package/dist/queue.js
CHANGED
|
@@ -1,297 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Structural comparison engine shared by every fixture matcher.
|
|
5
|
-
*
|
|
6
|
-
* Handles three kinds of "expected" values:
|
|
7
|
-
* - plain JSON values → strict deep equality
|
|
8
|
-
* - {@link Matcher} instances (code-side `match.*`)
|
|
9
|
-
* - strings containing `{{placeholder}}` forms (file-side fixtures)
|
|
10
|
-
*
|
|
11
|
-
* One unified `{{token}}` grammar (CONVENTIONS D4) — the same vocabulary
|
|
12
|
-
* works in `expected/*.http` (body and headers), `expected/*.json`, and
|
|
13
|
-
* text snapshots (`expected/*.txt`).
|
|
14
|
-
*
|
|
15
|
-
* Ref captures (`match.ref(name)` / `{{type#name}}`) are recorded in the
|
|
16
|
-
* {@link CaptureScope} supplied by the caller.
|
|
17
|
-
*/
|
|
18
|
-
const UUID_SOURCE = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
19
|
-
const ULID_SOURCE = "[0-9A-HJKMNP-TV-Z]{26}";
|
|
20
|
-
const ISO8601_SOURCE = String.raw`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})`;
|
|
21
|
-
const DATE_SOURCE = String.raw`\d{4}-\d{2}-\d{2}`;
|
|
22
|
-
const TIME_SOURCE = String.raw`\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?`;
|
|
23
|
-
const DURATION_SOURCE = String.raw`\d+(?:\.\d+)?(?:ms|s|m|h)`;
|
|
24
|
-
const NUMBER_SOURCE = String.raw`-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?`;
|
|
25
|
-
const INT_SOURCE = String.raw`-?\d+`;
|
|
26
|
-
const FLOAT_SOURCE = String.raw`-?\d+\.\d+`;
|
|
27
|
-
const SEMVER_SOURCE = String.raw`\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?`;
|
|
28
|
-
const SHA_SOURCE = "[0-9a-f]{7,64}";
|
|
29
|
-
const HEX_SOURCE = "[0-9a-fA-F]+";
|
|
30
|
-
const BASE64_SOURCE = "[A-Za-z0-9+/]+={0,2}";
|
|
31
|
-
const PORT_SOURCE = String.raw`(?:6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|\d{1,4})`;
|
|
32
|
-
const IP_OCTET = String.raw`(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)`;
|
|
33
|
-
const IP_SOURCE = String.raw`(?:${IP_OCTET}\.){3}${IP_OCTET}`;
|
|
34
|
-
const URL_SOURCE = String.raw`https?:\/\/[^\s"'<>]+`;
|
|
35
|
-
const EMBEDDED_SOURCES = {
|
|
36
|
-
base64: BASE64_SOURCE,
|
|
37
|
-
date: DATE_SOURCE,
|
|
38
|
-
duration: DURATION_SOURCE,
|
|
39
|
-
email: String.raw`[^\s@"'<>]+@[^\s@"'<>]+\.[^\s@"'<>]+`,
|
|
40
|
-
float: FLOAT_SOURCE,
|
|
41
|
-
hex: HEX_SOURCE,
|
|
42
|
-
int: INT_SOURCE,
|
|
43
|
-
ip: IP_SOURCE,
|
|
44
|
-
iso8601: ISO8601_SOURCE,
|
|
45
|
-
number: NUMBER_SOURCE,
|
|
46
|
-
path: String.raw`\.{0,2}\/[^\s"'<>]*`,
|
|
47
|
-
port: PORT_SOURCE,
|
|
48
|
-
semver: SEMVER_SOURCE,
|
|
49
|
-
sha: SHA_SOURCE,
|
|
50
|
-
time: TIME_SOURCE,
|
|
51
|
-
ulid: ULID_SOURCE,
|
|
52
|
-
url: URL_SOURCE,
|
|
53
|
-
uuid: UUID_SOURCE
|
|
54
|
-
};
|
|
55
|
-
const wholeRe = (source) => new RegExp(`^(?:${source})$`);
|
|
56
|
-
const WHOLE_RES = Object.fromEntries(Object.entries(EMBEDDED_SOURCES).map(([kind, source]) => [kind, wholeRe(source)]));
|
|
57
|
-
const PLACEHOLDER_RE = new RegExp(String.raw`\{\{(?<kind>${[...TOKEN_KINDS].sort((a, b) => b.length - a.length).join("|")})(?:#(?<ref>[\w.-]+))?\}\}`, "g");
|
|
58
|
-
/** Whether a fixture string contains at least one `{{placeholder}}`. */
|
|
59
|
-
function hasPlaceholders(value) {
|
|
60
|
-
PLACEHOLDER_RE.lastIndex = 0;
|
|
61
|
-
return PLACEHOLDER_RE.test(value);
|
|
62
|
-
}
|
|
63
|
-
/** Key-order-independent stringification for captured-object comparison. */
|
|
64
|
-
function stableStringify(value) {
|
|
65
|
-
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
66
|
-
if (value !== null && typeof value === "object") return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : 1).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
|
|
67
|
-
return JSON.stringify(value) ?? "undefined";
|
|
68
|
-
}
|
|
69
|
-
function capturedEquals(a, b) {
|
|
70
|
-
if (Object.is(a, b)) return true;
|
|
71
|
-
if (typeof a === "number" && typeof b === "string" || typeof a === "string" && typeof b === "number") return String(a) === String(b);
|
|
72
|
-
if (a !== null && b !== null && typeof a === "object" && typeof b === "object") return stableStringify(a) === stableStringify(b);
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
function recordRef(name, actual, scope) {
|
|
76
|
-
if (scope.has(name)) return capturedEquals(scope.get(name), actual);
|
|
77
|
-
scope.set(name, actual);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
function isPortValue(value) {
|
|
81
|
-
return Number.isInteger(value) && value >= 0 && value <= 65535;
|
|
82
|
-
}
|
|
83
|
-
function kindMatches(kind, actual, scope) {
|
|
84
|
-
switch (kind) {
|
|
85
|
-
case "any": return true;
|
|
86
|
-
case "float":
|
|
87
|
-
if (typeof actual === "number") return Number.isFinite(actual);
|
|
88
|
-
return typeof actual === "string" && WHOLE_RES.float.test(actual);
|
|
89
|
-
case "int":
|
|
90
|
-
if (typeof actual === "number") return Number.isInteger(actual);
|
|
91
|
-
return typeof actual === "string" && WHOLE_RES.int.test(actual);
|
|
92
|
-
case "number":
|
|
93
|
-
if (typeof actual === "number") return Number.isFinite(actual);
|
|
94
|
-
return typeof actual === "string" && WHOLE_RES.number.test(actual);
|
|
95
|
-
case "port":
|
|
96
|
-
if (typeof actual === "number") return isPortValue(actual);
|
|
97
|
-
return typeof actual === "string" && WHOLE_RES.port.test(actual) && isPortValue(Number(actual));
|
|
98
|
-
case "string": return typeof actual === "string";
|
|
99
|
-
case "workdir": return typeof actual === "string" && scope.workdir !== void 0 ? actual === scope.workdir : false;
|
|
100
|
-
default: {
|
|
101
|
-
const re = WHOLE_RES[kind];
|
|
102
|
-
return re !== void 0 && typeof actual === "string" && re.test(actual);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function matcherMatches(matcher, actual, scope) {
|
|
107
|
-
if (matcher.kind === "includes") return typeof actual === "string" && actual.includes(matcher.includes);
|
|
108
|
-
if (matcher.kind === "regex") return typeof actual === "string" && matcher.regex.test(actual);
|
|
109
|
-
if (matcher.kind === "ref") {
|
|
110
|
-
if (matcher.notRef && scope.has(matcher.notRef) && capturedEquals(scope.get(matcher.notRef), actual)) return false;
|
|
111
|
-
return recordRef(matcher.refName, actual, scope);
|
|
112
|
-
}
|
|
113
|
-
return kindMatches(matcher.kind, actual, scope);
|
|
114
|
-
}
|
|
115
|
-
function placeholderSource(kind, scope) {
|
|
116
|
-
if (kind === "workdir") return scope.workdir === void 0 ? "(?!)" : escapeRegExp(scope.workdir);
|
|
117
|
-
const source = EMBEDDED_SOURCES[kind];
|
|
118
|
-
if (source) return source;
|
|
119
|
-
return kind === "any" ? String.raw`[\s\S]*?` : String.raw`[^\n]*?`;
|
|
120
|
-
}
|
|
121
|
-
function escapeRegExp(text) {
|
|
122
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
123
|
-
}
|
|
124
|
-
function parsePlaceholderString(expected, scope) {
|
|
125
|
-
PLACEHOLDER_RE.lastIndex = 0;
|
|
126
|
-
const refs = [];
|
|
127
|
-
let source = "";
|
|
128
|
-
let lastIndex = 0;
|
|
129
|
-
let single = null;
|
|
130
|
-
let count = 0;
|
|
131
|
-
for (const found of expected.matchAll(PLACEHOLDER_RE)) {
|
|
132
|
-
const kind = found.groups.kind;
|
|
133
|
-
const ref = found.groups.ref;
|
|
134
|
-
source += escapeRegExp(expected.slice(lastIndex, found.index));
|
|
135
|
-
source += `(${placeholderSource(kind, scope)})`;
|
|
136
|
-
refs.push({
|
|
137
|
-
index: count,
|
|
138
|
-
kind,
|
|
139
|
-
ref
|
|
140
|
-
});
|
|
141
|
-
count++;
|
|
142
|
-
lastIndex = found.index + found[0].length;
|
|
143
|
-
if (found.index === 0 && found[0].length === expected.length) single = {
|
|
144
|
-
kind,
|
|
145
|
-
ref
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
source += escapeRegExp(expected.slice(lastIndex));
|
|
149
|
-
return {
|
|
150
|
-
pattern: new RegExp(`^${source}$`),
|
|
151
|
-
refs,
|
|
152
|
-
single,
|
|
153
|
-
source
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Unanchored regex source matching a fixture string — plain text escaped
|
|
158
|
-
* verbatim, each `{{token}}` expanded to its embedded grammar. Used to route
|
|
159
|
-
* declared `.http` intercept paths (`/articles/{{uuid}}`) as URL patterns.
|
|
160
|
-
*/
|
|
161
|
-
function placeholderPatternSource(expected) {
|
|
162
|
-
return parsePlaceholderString(expected, new CaptureScope()).source;
|
|
163
|
-
}
|
|
164
|
-
function placeholderStringMatches(expected, actual, scope) {
|
|
165
|
-
const parsed = parsePlaceholderString(expected, scope);
|
|
166
|
-
if (parsed.single) {
|
|
167
|
-
if (!kindMatches(parsed.single.kind, actual, scope)) return false;
|
|
168
|
-
if (parsed.single.ref) return recordRef(parsed.single.ref, actual, scope);
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
if (typeof actual !== "string" && typeof actual !== "number") return false;
|
|
172
|
-
const text = String(actual);
|
|
173
|
-
const found = parsed.pattern.exec(text);
|
|
174
|
-
if (!found) return false;
|
|
175
|
-
for (const entry of parsed.refs) {
|
|
176
|
-
if (!entry.ref) continue;
|
|
177
|
-
if (!recordRef(entry.ref, found[entry.index + 1], scope)) return false;
|
|
178
|
-
}
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
function isPlainObject(value) {
|
|
182
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Deep structural equality with matcher / placeholder support. Ref captures
|
|
186
|
-
* are recorded in `scope` in traversal order (arrays left-to-right, object
|
|
187
|
-
* keys in expected-key order).
|
|
188
|
-
*/
|
|
189
|
-
function structuralEquals(expected, actual, scope) {
|
|
190
|
-
if (expected instanceof Matcher) return matcherMatches(expected, actual, scope);
|
|
191
|
-
if (typeof expected === "string" && hasPlaceholders(expected)) return placeholderStringMatches(expected, actual, scope);
|
|
192
|
-
if (Array.isArray(expected)) {
|
|
193
|
-
if (!Array.isArray(actual) || actual.length !== expected.length) return false;
|
|
194
|
-
return expected.every((item, i) => structuralEquals(item, actual[i], scope));
|
|
195
|
-
}
|
|
196
|
-
if (isPlainObject(expected)) {
|
|
197
|
-
if (!isPlainObject(actual)) return false;
|
|
198
|
-
const expectedKeys = Object.keys(expected);
|
|
199
|
-
const actualKeys = Object.keys(actual);
|
|
200
|
-
if (expectedKeys.length !== actualKeys.length) return false;
|
|
201
|
-
return expectedKeys.every((key) => key in actual && structuralEquals(expected[key], actual[key], scope));
|
|
202
|
-
}
|
|
203
|
-
return Object.is(expected, actual);
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Deep structural SUBSET match (toMatchObject-style) with matcher /
|
|
207
|
-
* placeholder support. Plain objects match when every expected key is present
|
|
208
|
-
* and recursively subset-matches — the actual value may carry extra keys.
|
|
209
|
-
* Arrays require equal length with each element subset-matched. Leaves fall
|
|
210
|
-
* back to {@link structuralEquals} semantics (matchers, placeholders, strict
|
|
211
|
-
* equality). Used by request-body filters on intercept triggers.
|
|
212
|
-
*/
|
|
213
|
-
function structuralSubset(expected, actual, scope) {
|
|
214
|
-
if (expected instanceof Matcher) return matcherMatches(expected, actual, scope);
|
|
215
|
-
if (typeof expected === "string" && hasPlaceholders(expected)) return placeholderStringMatches(expected, actual, scope);
|
|
216
|
-
if (Array.isArray(expected)) {
|
|
217
|
-
if (!Array.isArray(actual) || actual.length !== expected.length) return false;
|
|
218
|
-
return expected.every((item, i) => structuralSubset(item, actual[i], scope));
|
|
219
|
-
}
|
|
220
|
-
if (isPlainObject(expected)) {
|
|
221
|
-
if (!isPlainObject(actual)) return false;
|
|
222
|
-
return Object.keys(expected).every((key) => key in actual && structuralSubset(expected[key], actual[key], scope));
|
|
223
|
-
}
|
|
224
|
-
return Object.is(expected, actual);
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Multi-line text comparison with `{{token}}` support — used by text
|
|
228
|
-
* snapshots (`expected/*.txt`). Without placeholders this is strict equality.
|
|
229
|
-
*/
|
|
230
|
-
function textEquals(expected, actual, scope) {
|
|
231
|
-
if (!hasPlaceholders(expected)) return expected === actual;
|
|
232
|
-
return placeholderStringMatches(expected, actual, scope);
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Render an expected value for failure diffs and serialization: Matcher
|
|
236
|
-
* instances become their placeholder text, everything else is untouched.
|
|
237
|
-
*/
|
|
238
|
-
function renderExpected(value) {
|
|
239
|
-
if (value instanceof Matcher) return value.toString();
|
|
240
|
-
if (Array.isArray(value)) return value.map((item) => renderExpected(item));
|
|
241
|
-
if (isPlainObject(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, renderExpected(item)]));
|
|
242
|
-
return value;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Substitute the framework-known cwd (`workdir`) for its `{{workdir}}` token in
|
|
246
|
-
* every string leaf of a structured value. The structural mirror of the text
|
|
247
|
-
* path's `actual.replaceAll(workdir, '{{workdir}}')` — so a golden written under
|
|
248
|
-
* `TEST_UPDATE=1` stores the token, not a run-specific temp path (CONVENTIONS
|
|
249
|
-
* D5). A no-op when `workdir` is undefined (no cwd, e.g. api/jobs mode).
|
|
250
|
-
*/
|
|
251
|
-
function substituteWorkdirDeep(value, workdir) {
|
|
252
|
-
if (typeof value === "string") return value.replaceAll(workdir, "{{workdir}}");
|
|
253
|
-
if (Array.isArray(value)) return value.map((item) => substituteWorkdirDeep(item, workdir));
|
|
254
|
-
if (isPlainObject(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, substituteWorkdirDeep(item, workdir)]));
|
|
255
|
-
return value;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Update-mode merge: rewrite a fixture from the actual output while
|
|
259
|
-
* preserving every segment of the previous fixture that was covered by a
|
|
260
|
-
* placeholder (and still matches the actual value).
|
|
261
|
-
*
|
|
262
|
-
* Token preservation is symmetric with the text path (CONVENTIONS D5): a
|
|
263
|
-
* previous `{{placeholder}}` (including `{{workdir}}`) that still matches the
|
|
264
|
-
* RAW actual value is kept; every other leaf is taken from the actual output
|
|
265
|
-
* with the framework-known cwd substituted back to `{{workdir}}`.
|
|
266
|
-
*/
|
|
267
|
-
function mergePreservingPlaceholders(previous, actual, workdir) {
|
|
268
|
-
const scope = new CaptureScope(workdir);
|
|
269
|
-
const subst = (value) => workdir === void 0 ? value : substituteWorkdirDeep(value, workdir);
|
|
270
|
-
function merge(prev, act) {
|
|
271
|
-
if (typeof prev === "string" && hasPlaceholders(prev)) return placeholderStringMatches(prev, act, scope) ? prev : subst(act);
|
|
272
|
-
if (Array.isArray(prev) && Array.isArray(act)) return act.map((item, i) => i < prev.length ? merge(prev[i], item) : subst(item));
|
|
273
|
-
if (isPlainObject(prev) && isPlainObject(act)) return Object.fromEntries(Object.entries(act).map(([key, item]) => [key, key in prev ? merge(prev[key], item) : subst(item)]));
|
|
274
|
-
return subst(act);
|
|
275
|
-
}
|
|
276
|
-
return merge(previous, actual);
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Update-mode merge for text snapshots: line-by-line, a previous line whose
|
|
280
|
-
* placeholders still match the actual line is preserved; every other line is
|
|
281
|
-
* taken from the actual output. Values the framework knows to be dynamic
|
|
282
|
-
* (`{{workdir}}`) are substituted automatically (CONVENTIONS D5).
|
|
283
|
-
*/
|
|
284
|
-
function mergeTextPreservingPlaceholders(previous, actual, scope) {
|
|
285
|
-
const substituted = scope.workdir === void 0 ? actual : actual.replaceAll(scope.workdir, "{{workdir}}");
|
|
286
|
-
if (previous === null) return substituted;
|
|
287
|
-
const prevLines = previous.split("\n");
|
|
288
|
-
return substituted.split("\n").map((line, i) => {
|
|
289
|
-
const prev = prevLines[i];
|
|
290
|
-
if (prev === void 0 || !hasPlaceholders(prev)) return line;
|
|
291
|
-
return textEquals(prev, line, new CaptureScope(scope.workdir)) || textEquals(prev, actual.split("\n")[i] ?? line, new CaptureScope(scope.workdir)) ? prev : line;
|
|
292
|
-
}).join("\n");
|
|
293
|
-
}
|
|
294
|
-
//#endregion
|
|
1
|
+
import { t as CaptureScope } from "./match.js";
|
|
2
|
+
import { A as placeholderPatternSource, D as hasPlaceholders, P as textEquals } from "./literate.js";
|
|
295
3
|
//#region src/core/contracts/contract.ts
|
|
296
4
|
/**
|
|
297
5
|
* Declare one contract. Identity function — its value is the enforced shape
|
|
@@ -552,4 +260,4 @@ var ContractQueue = class {
|
|
|
552
260
|
}
|
|
553
261
|
};
|
|
554
262
|
//#endregion
|
|
555
|
-
export { isContract as a,
|
|
263
|
+
export { isContract as a, defineContracts as i, contractsOf as n, isContracts as o, defineContract as r, ContractQueue as t };
|