@starci/eslint-canon-be 1.2.0 → 1.2.2
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/CHANGELOG.md +7 -0
- package/README.md +70 -69
- package/authorization.mjs +106 -106
- package/cdc.mjs +150 -150
- package/comments.mjs +388 -389
- package/cqrs.mjs +302 -303
- package/data-access.mjs +306 -307
- package/e2e-flow.mjs +222 -222
- package/event-delivery.mjs +93 -93
- package/exception-identity.mjs +200 -200
- package/exceptions.mjs +262 -265
- package/index.mjs +107 -107
- package/lib/path.mjs +2 -0
- package/module-layering.mjs +426 -432
- package/naming.mjs +219 -220
- package/observability.mjs +275 -275
- package/package.json +4 -2
- package/testing.mjs +500 -454
- package/transport.mjs +168 -168
- package/type-safety.mjs +510 -380
package/testing.mjs
CHANGED
|
@@ -1,454 +1,500 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules that hold `testing.md`.
|
|
3
|
-
*
|
|
4
|
-
* MOST OF THAT LAW IS NOT MACHINE-CHECKABLE, and pretending otherwise would be worse than
|
|
5
|
-
* checking nothing. No rule can tell whether a file represents a business flow, whether the
|
|
6
|
-
* unhappy path it covers drags a critical flow behind it, or whether the decision branches are
|
|
7
|
-
* covered - those are read by a person.
|
|
8
|
-
*
|
|
9
|
-
* What a rule CAN see is the two shapes that are wrong on their face regardless of intent: a spec
|
|
10
|
-
* whose every assertion is about a call rather than a result, and an end-to-end spec that never
|
|
11
|
-
* reads any state back. Both were measured before being written: eight specs and one e2e file in
|
|
12
|
-
* the source repository, which is why the second lands with a carve-out rather than at zero.
|
|
13
|
-
*
|
|
14
|
-
* Two more codes turned out to have a genuinely mechanical HALF, added later and kept narrow on
|
|
15
|
-
* purpose:
|
|
16
|
-
*
|
|
17
|
-
* - TESTING-1. A filename cannot be checked against a business sentence - but the anti-pattern
|
|
18
|
-
* the law names by example (`rewards-queries.e2e-spec.ts`) IS a closed, denylisted shape: a
|
|
19
|
-
* filename whose last segment is an API noun rather than a business one. The rule refuses only
|
|
20
|
-
* that shape; it does not claim to certify a good name, and it is the same requirement E2E-1 in
|
|
21
|
-
* `e2e-flow.md` states on the same file, so one rule holds both codes rather than reporting the
|
|
22
|
-
* same file twice under two names.
|
|
23
|
-
* - TESTING-7. "A stub of a model returns a payload the production parser can actually parse" -
|
|
24
|
-
* the rule cannot verify the payload parses, but it CAN refuse the one shape the law names by
|
|
25
|
-
* example: a bare marker string (`"stubbed"`, `"ok"`, `"test"`) standing in for the whole
|
|
26
|
-
* answer. An object literal, or a `JSON.stringify(...)` call, is out of the rule's reach on
|
|
27
|
-
* purpose - that is where a false positive would start.
|
|
28
|
-
*
|
|
29
|
-
* TESTING-5 / TESTING-8 ("a configured lane either holds tests or does not exist") was drafted as a
|
|
30
|
-
* filesystem-reading rule and then WITHDRAWN. It reads correctly as JS to `@typescript-eslint/parser`
|
|
31
|
-
* (`{"rootDir": "..."}` parses as an `ExpressionStatement` over an `ObjectExpression` under this
|
|
32
|
-
* parser, no wrapping needed) and its own file-scoped RuleTester cases passed. It is left out anyway,
|
|
33
|
-
* because the question it would answer is not "does this repository's config have this shape" but
|
|
34
|
-
* "does the CONSUMING repository's `eslint.config.mjs` even point a linter at this file at all" - and
|
|
35
|
-
* in the measured reference repository it does not: `files` there is five TypeScript globs under
|
|
36
|
-
* `src`, `apps`, `libs`, `test` and `tests` - none of them `.json` - so `jest-e2e.json` and
|
|
37
|
-
* `jest-harness.json` sit outside every one of them. A rule that cannot be reached by the config
|
|
38
|
-
* that would run it is not a `warn`-with-a-count away from correct; it is dead code reporting a "0"
|
|
39
|
-
* that means "never asked", not "already true" - the one measurement this family refuses to publish
|
|
40
|
-
* as if it were the other.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/** The fast lane: a plain unit spec, excluding every other suffix that also ends in `spec.ts`. */
|
|
47
|
-
const isUnitSpec = (filename) => {
|
|
48
|
-
const file = normalizePath(filename)
|
|
49
|
-
if (!/\.spec\.ts$/.test(file)) return false
|
|
50
|
-
return !/\.(?:e2e|int|harness)-spec\.ts$/.test(file)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** The flow lane. */
|
|
54
|
-
const isE2eSpec = (filename) => /\.e2e-spec\.ts$/.test(normalizePath(filename))
|
|
55
|
-
|
|
56
|
-
/** The paid model-quality lane. */
|
|
57
|
-
const isHarnessSpec = (filename) => /\.harness-spec\.ts$/.test(normalizePath(filename))
|
|
58
|
-
|
|
59
|
-
/** Test-only helpers whose only authority is the harness lane. */
|
|
60
|
-
const isHarnessHelper = (filename) => /\/src\/tests\/helpers\//.test(normalizePath(filename))
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Matchers that assert a CALL happened rather than what came out of it.
|
|
64
|
-
*
|
|
65
|
-
* Each is legitimate as a second assertion - "the mail went out" is a real observable effect. The
|
|
66
|
-
* rule fires only when a whole file has nothing else.
|
|
67
|
-
*/
|
|
68
|
-
const CALL_MATCHERS = new Set([
|
|
69
|
-
"toHaveBeenCalled",
|
|
70
|
-
"toHaveBeenCalledWith",
|
|
71
|
-
"toHaveBeenCalledTimes",
|
|
72
|
-
"toHaveBeenLastCalledWith",
|
|
73
|
-
"toHaveBeenNthCalledWith",
|
|
74
|
-
"toBeCalled",
|
|
75
|
-
"toBeCalledWith",
|
|
76
|
-
"toBeCalledTimes",
|
|
77
|
-
"toHaveReturned",
|
|
78
|
-
])
|
|
79
|
-
|
|
80
|
-
/** Names that mean the test read state back out of the database. */
|
|
81
|
-
const STATE_READERS = /^(?:entityManager|dataSource|EntityManager|DataSource|getRepository|queryRunner)$/
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* The matcher a given `expect(...)` call ends in.
|
|
85
|
-
*
|
|
86
|
-
* Climbs the member chain so modifiers pass through: `expect(x).not.toHaveBeenCalled()` answers
|
|
87
|
-
* `toHaveBeenCalled`, and `expect(p).resolves.toBe(1)` answers `toBe`.
|
|
88
|
-
*/
|
|
89
|
-
const matcherOf = (expectCall) => {
|
|
90
|
-
let cursor = expectCall
|
|
91
|
-
let last = null
|
|
92
|
-
while (cursor.parent && cursor.parent.type === "MemberExpression" && cursor.parent.object === cursor) {
|
|
93
|
-
last = cursor.parent.property && cursor.parent.property.name
|
|
94
|
-
cursor = cursor.parent
|
|
95
|
-
}
|
|
96
|
-
return last
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// -- TESTING-6 -------------------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
/** A spec whose every assertion is about a call restates the source instead of testing it. */
|
|
102
|
-
export const noCallOnlySpec = {
|
|
103
|
-
meta: {
|
|
104
|
-
type: "problem",
|
|
105
|
-
docs: { description: "A unit spec asserts a result or a state change, not only that a call happened." },
|
|
106
|
-
schema: [],
|
|
107
|
-
messages: {
|
|
108
|
-
callOnly:
|
|
109
|
-
"Every assertion in this spec is `{{matchers}}` - it restates the handler's own source. Rename a collaborator's method and this file goes red; change the business rule to a wrong value and it stays green. Assert what came back, or what changed. A call assertion is legitimate as a SECOND assertion, where the call itself is the observable effect.",
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
create(context) {
|
|
113
|
-
if (!isUnitSpec(context.filename || context.getFilename())) return {}
|
|
114
|
-
let assertions = 0
|
|
115
|
-
let callAssertions = 0
|
|
116
|
-
const seen = new Set()
|
|
117
|
-
return {
|
|
118
|
-
CallExpression(node) {
|
|
119
|
-
if (!node.callee || node.callee.type !== "Identifier" || node.callee.name !== "expect") return
|
|
120
|
-
const matcher = matcherOf(node)
|
|
121
|
-
if (matcher === null) return
|
|
122
|
-
assertions += 1
|
|
123
|
-
if (!CALL_MATCHERS.has(matcher)) return
|
|
124
|
-
callAssertions += 1
|
|
125
|
-
seen.add(matcher)
|
|
126
|
-
},
|
|
127
|
-
"Program:exit"(node) {
|
|
128
|
-
// the file asserts nothing at all, or asserts something other than a call somewhere
|
|
129
|
-
if (assertions === 0 || callAssertions !== assertions) return
|
|
130
|
-
context.report({ node, messageId: "callOnly", data: { matchers: [...seen].join(", ") } })
|
|
131
|
-
},
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// -- TESTING-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `testing.md`.
|
|
3
|
+
*
|
|
4
|
+
* MOST OF THAT LAW IS NOT MACHINE-CHECKABLE, and pretending otherwise would be worse than
|
|
5
|
+
* checking nothing. No rule can tell whether a file represents a business flow, whether the
|
|
6
|
+
* unhappy path it covers drags a critical flow behind it, or whether the decision branches are
|
|
7
|
+
* covered - those are read by a person.
|
|
8
|
+
*
|
|
9
|
+
* What a rule CAN see is the two shapes that are wrong on their face regardless of intent: a spec
|
|
10
|
+
* whose every assertion is about a call rather than a result, and an end-to-end spec that never
|
|
11
|
+
* reads any state back. Both were measured before being written: eight specs and one e2e file in
|
|
12
|
+
* the source repository, which is why the second lands with a carve-out rather than at zero.
|
|
13
|
+
*
|
|
14
|
+
* Two more codes turned out to have a genuinely mechanical HALF, added later and kept narrow on
|
|
15
|
+
* purpose:
|
|
16
|
+
*
|
|
17
|
+
* - TESTING-1. A filename cannot be checked against a business sentence - but the anti-pattern
|
|
18
|
+
* the law names by example (`rewards-queries.e2e-spec.ts`) IS a closed, denylisted shape: a
|
|
19
|
+
* filename whose last segment is an API noun rather than a business one. The rule refuses only
|
|
20
|
+
* that shape; it does not claim to certify a good name, and it is the same requirement E2E-1 in
|
|
21
|
+
* `e2e-flow.md` states on the same file, so one rule holds both codes rather than reporting the
|
|
22
|
+
* same file twice under two names.
|
|
23
|
+
* - TESTING-7. "A stub of a model returns a payload the production parser can actually parse" -
|
|
24
|
+
* the rule cannot verify the payload parses, but it CAN refuse the one shape the law names by
|
|
25
|
+
* example: a bare marker string (`"stubbed"`, `"ok"`, `"test"`) standing in for the whole
|
|
26
|
+
* answer. An object literal, or a `JSON.stringify(...)` call, is out of the rule's reach on
|
|
27
|
+
* purpose - that is where a false positive would start.
|
|
28
|
+
*
|
|
29
|
+
* TESTING-5 / TESTING-8 ("a configured lane either holds tests or does not exist") was drafted as a
|
|
30
|
+
* filesystem-reading rule and then WITHDRAWN. It reads correctly as JS to `@typescript-eslint/parser`
|
|
31
|
+
* (`{"rootDir": "..."}` parses as an `ExpressionStatement` over an `ObjectExpression` under this
|
|
32
|
+
* parser, no wrapping needed) and its own file-scoped RuleTester cases passed. It is left out anyway,
|
|
33
|
+
* because the question it would answer is not "does this repository's config have this shape" but
|
|
34
|
+
* "does the CONSUMING repository's `eslint.config.mjs` even point a linter at this file at all" - and
|
|
35
|
+
* in the measured reference repository it does not: `files` there is five TypeScript globs under
|
|
36
|
+
* `src`, `apps`, `libs`, `test` and `tests` - none of them `.json` - so `jest-e2e.json` and
|
|
37
|
+
* `jest-harness.json` sit outside every one of them. A rule that cannot be reached by the config
|
|
38
|
+
* that would run it is not a `warn`-with-a-count away from correct; it is dead code reporting a "0"
|
|
39
|
+
* that means "never asked", not "already true" - the one measurement this family refuses to publish
|
|
40
|
+
* as if it were the other.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { statSync } from "node:fs"
|
|
44
|
+
import { normalizePath } from "./lib/path.mjs"
|
|
45
|
+
|
|
46
|
+
/** The fast lane: a plain unit spec, excluding every other suffix that also ends in `spec.ts`. */
|
|
47
|
+
const isUnitSpec = (filename) => {
|
|
48
|
+
const file = normalizePath(filename)
|
|
49
|
+
if (!/\.spec\.ts$/.test(file)) return false
|
|
50
|
+
return !/\.(?:e2e|int|harness)-spec\.ts$/.test(file)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The flow lane. */
|
|
54
|
+
const isE2eSpec = (filename) => /\.e2e-spec\.ts$/.test(normalizePath(filename))
|
|
55
|
+
|
|
56
|
+
/** The paid model-quality lane. */
|
|
57
|
+
const isHarnessSpec = (filename) => /\.harness-spec\.ts$/.test(normalizePath(filename))
|
|
58
|
+
|
|
59
|
+
/** Test-only helpers whose only authority is the harness lane. */
|
|
60
|
+
const isHarnessHelper = (filename) => /\/src\/tests\/helpers\//.test(normalizePath(filename))
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Matchers that assert a CALL happened rather than what came out of it.
|
|
64
|
+
*
|
|
65
|
+
* Each is legitimate as a second assertion - "the mail went out" is a real observable effect. The
|
|
66
|
+
* rule fires only when a whole file has nothing else.
|
|
67
|
+
*/
|
|
68
|
+
const CALL_MATCHERS = new Set([
|
|
69
|
+
"toHaveBeenCalled",
|
|
70
|
+
"toHaveBeenCalledWith",
|
|
71
|
+
"toHaveBeenCalledTimes",
|
|
72
|
+
"toHaveBeenLastCalledWith",
|
|
73
|
+
"toHaveBeenNthCalledWith",
|
|
74
|
+
"toBeCalled",
|
|
75
|
+
"toBeCalledWith",
|
|
76
|
+
"toBeCalledTimes",
|
|
77
|
+
"toHaveReturned",
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
/** Names that mean the test read state back out of the database. */
|
|
81
|
+
const STATE_READERS = /^(?:entityManager|dataSource|EntityManager|DataSource|getRepository|queryRunner)$/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The matcher a given `expect(...)` call ends in.
|
|
85
|
+
*
|
|
86
|
+
* Climbs the member chain so modifiers pass through: `expect(x).not.toHaveBeenCalled()` answers
|
|
87
|
+
* `toHaveBeenCalled`, and `expect(p).resolves.toBe(1)` answers `toBe`.
|
|
88
|
+
*/
|
|
89
|
+
const matcherOf = (expectCall) => {
|
|
90
|
+
let cursor = expectCall
|
|
91
|
+
let last = null
|
|
92
|
+
while (cursor.parent && cursor.parent.type === "MemberExpression" && cursor.parent.object === cursor) {
|
|
93
|
+
last = cursor.parent.property && cursor.parent.property.name
|
|
94
|
+
cursor = cursor.parent
|
|
95
|
+
}
|
|
96
|
+
return last
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -- TESTING-6 -------------------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/** A spec whose every assertion is about a call restates the source instead of testing it. */
|
|
102
|
+
export const noCallOnlySpec = {
|
|
103
|
+
meta: {
|
|
104
|
+
type: "problem",
|
|
105
|
+
docs: { description: "A unit spec asserts a result or a state change, not only that a call happened." },
|
|
106
|
+
schema: [],
|
|
107
|
+
messages: {
|
|
108
|
+
callOnly:
|
|
109
|
+
"Every assertion in this spec is `{{matchers}}` - it restates the handler's own source. Rename a collaborator's method and this file goes red; change the business rule to a wrong value and it stays green. Assert what came back, or what changed. A call assertion is legitimate as a SECOND assertion, where the call itself is the observable effect.",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
create(context) {
|
|
113
|
+
if (!isUnitSpec(context.filename || context.getFilename())) return {}
|
|
114
|
+
let assertions = 0
|
|
115
|
+
let callAssertions = 0
|
|
116
|
+
const seen = new Set()
|
|
117
|
+
return {
|
|
118
|
+
CallExpression(node) {
|
|
119
|
+
if (!node.callee || node.callee.type !== "Identifier" || node.callee.name !== "expect") return
|
|
120
|
+
const matcher = matcherOf(node)
|
|
121
|
+
if (matcher === null) return
|
|
122
|
+
assertions += 1
|
|
123
|
+
if (!CALL_MATCHERS.has(matcher)) return
|
|
124
|
+
callAssertions += 1
|
|
125
|
+
seen.add(matcher)
|
|
126
|
+
},
|
|
127
|
+
"Program:exit"(node) {
|
|
128
|
+
// the file asserts nothing at all, or asserts something other than a call somewhere
|
|
129
|
+
if (assertions === 0 || callAssertions !== assertions) return
|
|
130
|
+
context.report({ node, messageId: "callOnly", data: { matchers: [...seen].join(", ") } })
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// -- TESTING-7 -------------------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
const UNIT_TEST_BUCKET = /\/(?:src\/tests|tests?\/unit)(?:\/|$)/
|
|
139
|
+
const HFS_TEST_CATEGORY = /\/src\/tests\/(?:integration|fixtures|harness|e2e)\//
|
|
140
|
+
const SUBJECT_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs"]
|
|
141
|
+
|
|
142
|
+
/** A structural test in an HFS test category may test a source file beside it. */
|
|
143
|
+
const isColocatedTestInfrastructureSpec = (filename) => {
|
|
144
|
+
if (!HFS_TEST_CATEGORY.test(filename) || !isUnitSpec(filename)) return false
|
|
145
|
+
const subject = filename.slice(0, -".spec.ts".length)
|
|
146
|
+
return SUBJECT_EXTENSIONS.some((extension) => {
|
|
147
|
+
try {
|
|
148
|
+
return statSync(`${subject}${extension}`).isFile()
|
|
149
|
+
} catch {
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Backend units are colocated `.spec.ts` files, including structural test infrastructure. */
|
|
156
|
+
export const unitTestColocated = {
|
|
157
|
+
meta: {
|
|
158
|
+
type: "problem",
|
|
159
|
+
docs: { description: "Backend unit tests are colocated `.spec.ts` files; generic `.test.ts` and non-colocated unit buckets are forbidden." },
|
|
160
|
+
schema: [],
|
|
161
|
+
messages: {
|
|
162
|
+
suffix: "`{{name}}` uses `.test.ts`. A backend unit is a colocated `.spec.ts` file beside its production owner.",
|
|
163
|
+
bucket: "`{{path}}` files a unit away from its subject. Colocate it with the owner, or place a structural spec beside the test-infrastructure file it checks in an HFS src/tests category.",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
create(context) {
|
|
167
|
+
const file = normalizePath(context.filename || context.getFilename())
|
|
168
|
+
const genericUnit = /\.test\.ts$/.test(file)
|
|
169
|
+
const bucketedUnit = isUnitSpec(file) && UNIT_TEST_BUCKET.test(file) && !isColocatedTestInfrastructureSpec(file)
|
|
170
|
+
if (!genericUnit && !bucketedUnit) return {}
|
|
171
|
+
return {
|
|
172
|
+
Program(node) {
|
|
173
|
+
if (genericUnit) context.report({ node, messageId: "suffix", data: { name: file.slice(file.lastIndexOf("/") + 1) } })
|
|
174
|
+
if (bucketedUnit) context.report({ node, messageId: "bucket", data: { path: file } })
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// -- TESTING-2 -------------------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/** An end-to-end spec that never reads state back proves only that the server replied. */
|
|
183
|
+
export const e2eAssertsPersistedState = {
|
|
184
|
+
meta: {
|
|
185
|
+
type: "problem",
|
|
186
|
+
docs: { description: "An e2e spec reads state back rather than asserting only on the response." },
|
|
187
|
+
schema: [],
|
|
188
|
+
messages: {
|
|
189
|
+
noState:
|
|
190
|
+
"This e2e never reads any state back - it asserts on responses alone, so the flow can stop persisting and this file stays green. Read the row, the balance or the entitlement out of the database and assert THAT. A flow that genuinely has no persisted consequence needs an eslint-disable naming what it observes instead.",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
create(context) {
|
|
194
|
+
if (!isE2eSpec(context.filename || context.getFilename())) return {}
|
|
195
|
+
let readsState = false
|
|
196
|
+
return {
|
|
197
|
+
Identifier(node) {
|
|
198
|
+
if (STATE_READERS.test(node.name)) readsState = true
|
|
199
|
+
},
|
|
200
|
+
"Program:exit"(node) {
|
|
201
|
+
if (readsState) return
|
|
202
|
+
context.report({ node, messageId: "noState" })
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// -- TESTING-9 -------------------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
/** Provider SDKs. Importing one into a flow test is a real model call by any other name. */
|
|
211
|
+
const PROVIDER_PACKAGES = /^(?:@anthropic-ai\/|openai$|openai\/|ollama$|@google\/generative-ai|@mistralai\/|cohere-ai)/
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The harness's own model helpers, which exist to reach a provider.
|
|
215
|
+
*
|
|
216
|
+
* Matched WITHOUT the `tests/` prefix: an e2e sits beside the helper folder, so the import it
|
|
217
|
+
* actually writes is the relative `../helpers/models.service`, and a pattern anchored on the
|
|
218
|
+
* absolute path never sees it.
|
|
219
|
+
*/
|
|
220
|
+
const HARNESS_MODEL_HELPERS = /helpers\/models(?:\.service)?$/
|
|
221
|
+
|
|
222
|
+
/** An e2e never calls a model; the harness is the only lane that does. */
|
|
223
|
+
export const noModelCallInE2e = {
|
|
224
|
+
meta: {
|
|
225
|
+
type: "problem",
|
|
226
|
+
docs: { description: "An e2e spec never reaches a model provider." },
|
|
227
|
+
schema: [],
|
|
228
|
+
messages: {
|
|
229
|
+
provider:
|
|
230
|
+
"`{{source}}` reaches a model provider from an e2e. A model call costs money, takes seconds and answers differently every time - so this makes the flow suite expensive, slow and flaky at once, and the assertion has to be loosened until it stops catching anything. Stub the model and assert what can actually break: the entitlement, the quota, the persisted answer. Judging the answer itself belongs in the harness.",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
create(context) {
|
|
234
|
+
if (!isE2eSpec(context.filename || context.getFilename())) return {}
|
|
235
|
+
return {
|
|
236
|
+
ImportDeclaration(node) {
|
|
237
|
+
const source = node.source && node.source.value
|
|
238
|
+
if (typeof source !== "string") return
|
|
239
|
+
if (!PROVIDER_PACKAGES.test(source) && !HARNESS_MODEL_HELPERS.test(source)) return
|
|
240
|
+
context.report({ node, messageId: "provider", data: { source } })
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// -- TESTING-10 ------------------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
/** Official provider clients that a harness may call without a house routing layer. */
|
|
249
|
+
const DIRECT_PROVIDER_PACKAGES = /^(?:@anthropic-ai\/sdk(?:\/|$)|openai(?:\/|$)|@google\/genai(?:\/|$)|@google\/generative-ai(?:\/|$)|@mistralai\/(?:mistralai|mistralai-ts)(?:\/|$)|cohere-ai(?:\/|$)|ollama(?:\/|$))/
|
|
250
|
+
|
|
251
|
+
/** Helpers that hide a model call or impersonate the production gateway. */
|
|
252
|
+
const FORBIDDEN_HARNESS_HELPERS = /(?:^|\/)(?:models(?:\.service)?|harness-invoke(?:\.service)?)$/
|
|
253
|
+
|
|
254
|
+
/** Consumer/CLI credential authorities that are never provider API credentials. */
|
|
255
|
+
const FORBIDDEN_HARNESS_AUTH = /(?:CLAUDE_CODE_OAUTH_TOKEN|claude-code-token|sk-ant-oat|OAUTH_BETA|CHATGPT_(?:SESSION|AUTH)_TOKEN|CODEX_(?:SESSION|AUTH)_TOKEN|auth-profile)/i
|
|
256
|
+
|
|
257
|
+
/** Imported symbols that reveal a fake production gateway in the harness lane. */
|
|
258
|
+
const FORBIDDEN_HARNESS_SYMBOLS = new Set([
|
|
259
|
+
"AiInvokeService",
|
|
260
|
+
"HarnessInvokeService",
|
|
261
|
+
"createHarnessInvoke",
|
|
262
|
+
])
|
|
263
|
+
|
|
264
|
+
/** Return the static property name of an object/member property when one exists. */
|
|
265
|
+
const propertyName = (property) => {
|
|
266
|
+
if (!property) return null
|
|
267
|
+
if (property.type === "Identifier") return property.name
|
|
268
|
+
if (property.type === "Literal") return property.value
|
|
269
|
+
return null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Find the `AiInvokeService` token in a `Pick<AiInvokeService, ...>` type. */
|
|
273
|
+
const aiInvokePickToken = (sourceCode) => {
|
|
274
|
+
const tokens = sourceCode.getTokens(sourceCode.ast)
|
|
275
|
+
for (let index = 0; index < tokens.length - 2; index += 1) {
|
|
276
|
+
if (tokens[index].value !== "Pick" || tokens[index + 1].value !== "<") continue
|
|
277
|
+
if (tokens[index + 2].value === "AiInvokeService") return tokens[index + 2]
|
|
278
|
+
}
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A harness calls one explicit provider SDK and never disguises it as `AiInvokeService`. */
|
|
283
|
+
export const harnessCallsProviderDirectly = {
|
|
284
|
+
meta: {
|
|
285
|
+
type: "problem",
|
|
286
|
+
docs: { description: "A model-quality harness calls an approved provider SDK directly with provider API credentials." },
|
|
287
|
+
schema: [],
|
|
288
|
+
messages: {
|
|
289
|
+
missingProvider:
|
|
290
|
+
"This harness imports no approved provider SDK. A model-quality harness calls the declared provider client directly; a house helper, tier or gateway override can make it green about a model production does not use.",
|
|
291
|
+
gateway:
|
|
292
|
+
"`{{name}}` impersonates or replaces the production AI gateway from a harness. Reuse the production prompt builder and parser around a direct provider SDK call instead.",
|
|
293
|
+
helper:
|
|
294
|
+
"`{{source}}` hides the provider call behind a house harness helper. Import and call the approved provider SDK in this harness; only credential loading may be shared.",
|
|
295
|
+
consumerAuth:
|
|
296
|
+
"`{{authority}}` is a consumer or CLI credential authority, not a provider-issued server API key. Read an explicit harness API-key environment variable instead.",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
create(context) {
|
|
300
|
+
const filename = context.filename || context.getFilename()
|
|
301
|
+
const harness = isHarnessSpec(filename)
|
|
302
|
+
const authScope = harness || isHarnessHelper(filename)
|
|
303
|
+
if (!authScope) return {}
|
|
304
|
+
|
|
305
|
+
let hasProviderImport = false
|
|
306
|
+
return {
|
|
307
|
+
ImportDeclaration(node) {
|
|
308
|
+
const source = node.source && node.source.value
|
|
309
|
+
if (typeof source !== "string") return
|
|
310
|
+
if (harness && DIRECT_PROVIDER_PACKAGES.test(source)) hasProviderImport = true
|
|
311
|
+
if (harness && FORBIDDEN_HARNESS_HELPERS.test(source.replace(/\.(?:ts|js)$/, ""))) {
|
|
312
|
+
context.report({ node, messageId: "helper", data: { source } })
|
|
313
|
+
}
|
|
314
|
+
if (!harness) return
|
|
315
|
+
for (const specifier of node.specifiers) {
|
|
316
|
+
const imported = specifier.imported && (specifier.imported.name || specifier.imported.value)
|
|
317
|
+
const local = specifier.local && specifier.local.name
|
|
318
|
+
const name = imported || local
|
|
319
|
+
if (!FORBIDDEN_HARNESS_SYMBOLS.has(name)) continue
|
|
320
|
+
context.report({ node: specifier, messageId: "gateway", data: { name } })
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
Literal(node) {
|
|
324
|
+
if (typeof node.value !== "string" || !FORBIDDEN_HARNESS_AUTH.test(node.value)) return
|
|
325
|
+
context.report({ node, messageId: "consumerAuth", data: { authority: node.value } })
|
|
326
|
+
},
|
|
327
|
+
Property(node) {
|
|
328
|
+
if (!harness || propertyName(node.key) !== "provide") return
|
|
329
|
+
if (!node.value || node.value.type !== "Identifier" || node.value.name !== "AiInvokeService") return
|
|
330
|
+
context.report({ node, messageId: "gateway", data: { name: "provide: AiInvokeService" } })
|
|
331
|
+
},
|
|
332
|
+
CallExpression(node) {
|
|
333
|
+
if (!harness || !node.callee || node.callee.type !== "MemberExpression") return
|
|
334
|
+
if (propertyName(node.callee.property) !== "overrideProvider") return
|
|
335
|
+
const argument = node.arguments && node.arguments[0]
|
|
336
|
+
if (!argument || argument.type !== "Identifier" || argument.name !== "AiInvokeService") return
|
|
337
|
+
context.report({ node, messageId: "gateway", data: { name: "overrideProvider(AiInvokeService)" } })
|
|
338
|
+
},
|
|
339
|
+
"Program:exit"(node) {
|
|
340
|
+
if (!harness) return
|
|
341
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
342
|
+
const pickToken = aiInvokePickToken(sourceCode)
|
|
343
|
+
if (pickToken) {
|
|
344
|
+
context.report({ node: pickToken, messageId: "gateway", data: { name: 'Pick<AiInvokeService, "run">' } })
|
|
345
|
+
}
|
|
346
|
+
if (!hasProviderImport) context.report({ node, messageId: "missingProvider" })
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// -- TESTING-1 (and E2E-1 in e2e-flow.md, the same requirement on the same file) -------------------
|
|
353
|
+
|
|
354
|
+
/** Nouns that name an API shape rather than a business promise. `rewards-queries` is the anchor. */
|
|
355
|
+
const API_SHAPED_FILENAME_NOUNS = new Set([
|
|
356
|
+
"queries", "query", "mutations", "mutation",
|
|
357
|
+
"resolvers", "resolver", "endpoints", "endpoint",
|
|
358
|
+
"controllers", "controller", "handlers", "handler",
|
|
359
|
+
"modules", "module", "apis", "api",
|
|
360
|
+
])
|
|
361
|
+
|
|
362
|
+
/** The hyphen-separated words of an e2e filename, with the lane suffix stripped. */
|
|
363
|
+
const e2eFilenameSegments = (filename) => {
|
|
364
|
+
const base = normalizePath(filename).split("/").pop() || ""
|
|
365
|
+
return base.replace(/\.e2e-spec\.ts$/, "").split("-").filter(Boolean)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** An e2e file is one business flow; the filename may not be an API-shape noun wearing its clothes. */
|
|
369
|
+
export const noApiShapedE2eFilename = {
|
|
370
|
+
meta: {
|
|
371
|
+
type: "problem",
|
|
372
|
+
docs: {
|
|
373
|
+
description:
|
|
374
|
+
"An e2e filename reads as a business sentence (TESTING-1; E2E-1 in e2e-flow.md), not a resolver/endpoint/module group.",
|
|
375
|
+
},
|
|
376
|
+
schema: [],
|
|
377
|
+
messages: {
|
|
378
|
+
apiShaped:
|
|
379
|
+
"`{{segment}}` names an API shape - a resolver group, an endpoint, a module - not a business promise. TESTING-1 / E2E-1: one e2e file is one business flow and the filename IS that flow; `rewards-queries.e2e-spec.ts` is exactly the shape this refuses. Name the file for the sentence it proves (`course-purchase.e2e-spec.ts`), not the API surface it happens to hit.",
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
create(context) {
|
|
383
|
+
const filename = context.filename || context.getFilename()
|
|
384
|
+
if (!isE2eSpec(filename)) return {}
|
|
385
|
+
const segments = e2eFilenameSegments(filename)
|
|
386
|
+
if (segments.length === 0) return {}
|
|
387
|
+
const last = segments[segments.length - 1].toLowerCase()
|
|
388
|
+
if (!API_SHAPED_FILENAME_NOUNS.has(last)) return {}
|
|
389
|
+
return {
|
|
390
|
+
Program(node) {
|
|
391
|
+
context.report({ node, messageId: "apiShaped", data: { segment: last } })
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// -- TESTING-7 -------------------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/** Test-infra helpers, where the shared model stub this code governs actually lives. */
|
|
400
|
+
const isTestHelperFile = (filename) => /\/tests\/helpers\//.test(normalizePath(filename))
|
|
401
|
+
|
|
402
|
+
/** Bare markers a stub returns when nobody gave it a real answer to stand in for. */
|
|
403
|
+
const MARKER_STRINGS = new Set(["stubbed", "stub", "ok", "test", "mock", "fake", "todo", "tbd", "n/a", "pending", ""])
|
|
404
|
+
|
|
405
|
+
/** A string literal that is nothing but a marker - not an object, not a `JSON.stringify(...)` call. */
|
|
406
|
+
const isMarkerLiteral = (node) => Boolean(
|
|
407
|
+
node && node.type === "Literal" && typeof node.value === "string" && MARKER_STRINGS.has(node.value.trim().toLowerCase()),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
/** The world's default model stub returns a payload the production parser can actually parse. */
|
|
411
|
+
export const noMarkerModelStub = {
|
|
412
|
+
meta: {
|
|
413
|
+
type: "problem",
|
|
414
|
+
docs: {
|
|
415
|
+
description: "A model stub in test infra returns a parseable payload (TESTING-7), never a bare marker string.",
|
|
416
|
+
},
|
|
417
|
+
schema: [],
|
|
418
|
+
messages: {
|
|
419
|
+
marker:
|
|
420
|
+
"This stub resolves to `{{value}}`, a marker rather than an answer. TESTING-7: a stub of a model returns a payload the production parser can actually parse - a marker string means the strict-JSON parser the real flow depends on never runs, so the most fragile seam in the flow is never exercised. Return the shape the production parser expects (an object, or a `JSON.stringify(...)` of one), the way `DEFAULT_MODEL_ANSWER` does.",
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
create(context) {
|
|
424
|
+
if (!isTestHelperFile(context.filename || context.getFilename())) return {}
|
|
425
|
+
return {
|
|
426
|
+
CallExpression(node) {
|
|
427
|
+
const callee = node.callee
|
|
428
|
+
if (!callee || callee.type !== "MemberExpression" || callee.computed) return
|
|
429
|
+
const method = callee.property.name
|
|
430
|
+
if (method === "mockResolvedValue" || method === "mockReturnValue") {
|
|
431
|
+
const argument = node.arguments[0]
|
|
432
|
+
if (isMarkerLiteral(argument)) {
|
|
433
|
+
context.report({ node: argument, messageId: "marker", data: { value: JSON.stringify(argument.value) } })
|
|
434
|
+
}
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
if (method !== "mockImplementation") return
|
|
438
|
+
const fn = node.arguments[0]
|
|
439
|
+
if (!fn || (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression")) return
|
|
440
|
+
if (fn.body.type !== "BlockStatement") {
|
|
441
|
+
if (isMarkerLiteral(fn.body)) {
|
|
442
|
+
context.report({ node: fn.body, messageId: "marker", data: { value: JSON.stringify(fn.body.value) } })
|
|
443
|
+
}
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
for (const statement of fn.body.body) {
|
|
447
|
+
if (statement.type === "ReturnStatement" && isMarkerLiteral(statement.argument)) {
|
|
448
|
+
context.report({ node: statement.argument, messageId: "marker", data: { value: JSON.stringify(statement.argument.value) } })
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** The rules this law contributes to the plugin. */
|
|
457
|
+
export const rules = {
|
|
458
|
+
"no-call-only-spec": noCallOnlySpec,
|
|
459
|
+
"unit-test-colocated": unitTestColocated,
|
|
460
|
+
"e2e-asserts-persisted-state": e2eAssertsPersistedState,
|
|
461
|
+
"no-model-call-in-e2e": noModelCallInE2e,
|
|
462
|
+
"harness-calls-provider-directly": harnessCallsProviderDirectly,
|
|
463
|
+
"no-api-shaped-e2e-filename": noApiShapedE2eFilename,
|
|
464
|
+
"no-marker-model-stub": noMarkerModelStub,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The level this law asks for, as the plugin's own opinion.
|
|
469
|
+
*
|
|
470
|
+
* MEASURED WITH THE RULES THEMSELVES against the reference repository, which is the only count
|
|
471
|
+
* worth writing down: 1 call-only spec of 181, and 1 e2e of 48 that never reads state back.
|
|
472
|
+
*
|
|
473
|
+
* That number was wrong twice before it was right, and both ways are worth knowing. A grep-based
|
|
474
|
+
* estimate said EIGHT - it counted files whose assertions merely INCLUDED a call matcher, which is
|
|
475
|
+
* the shape this rule deliberately permits. A first run of the rule itself said THREE - it counted
|
|
476
|
+
* eslint's own complaints about inline `eslint-disable` comments referring to rules the measuring
|
|
477
|
+
* config never loaded. Count only the reports carrying this rule's own id.
|
|
478
|
+
*
|
|
479
|
+
* Both land at `warn` with the count, get burned down, and flip to `error` at zero.
|
|
480
|
+
*
|
|
481
|
+
* The two later rules were measured the same way, against the same repository:
|
|
482
|
+
*
|
|
483
|
+
* - `no-api-shaped-e2e-filename` found TWO of 77 - `installment-plan-queries.e2e-spec.ts` and
|
|
484
|
+
* `rewards-queries.e2e-spec.ts`, the exact anchor example the law names. Real, pre-existing debt
|
|
485
|
+
* this task does not fix, so it lands at `warn` rather than claiming a burn-down that has not
|
|
486
|
+
* happened.
|
|
487
|
+
* - `no-marker-model-stub` found ZERO of the world's own stub in `tests/helpers/flow-world.ts`:
|
|
488
|
+
* the default answer is `JSON.stringify({ answer: "..." })`, which is a `CallExpression`, not a
|
|
489
|
+
* `Literal`, so the rule does not even see it as a candidate. It ships at `error` as a
|
|
490
|
+
* regression guard on a shape that is already correct.
|
|
491
|
+
*/
|
|
492
|
+
export const recommended = {
|
|
493
|
+
"starci-be/no-call-only-spec": "error", // no=0 of 182 - burned down from 1
|
|
494
|
+
"starci-be/unit-test-colocated": "error",
|
|
495
|
+
"starci-be/e2e-asserts-persisted-state": "error", // no=0 of 47 - burned down from 1
|
|
496
|
+
"starci-be/no-model-call-in-e2e": "error", // no=0 of 47
|
|
497
|
+
"starci-be/harness-calls-provider-directly": "error",
|
|
498
|
+
"starci-be/no-api-shaped-e2e-filename": "warn", // no=2 of 77 - pre-existing, not burned down here
|
|
499
|
+
"starci-be/no-marker-model-stub": "error", // no=0 - the world's stub already returns JSON.stringify(...)
|
|
500
|
+
}
|