@pyreon/compiler 0.13.1 → 0.15.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 +14 -4
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1330 -409
- package/lib/types/index.d.ts +152 -14
- package/package.json +12 -1
- package/src/event-names.ts +65 -0
- package/src/index.ts +10 -1
- package/src/jsx.ts +974 -784
- package/src/pyreon-intercept.ts +728 -0
- package/src/test-audit.ts +435 -0
- package/src/tests/depth-stress.test.ts +16 -0
- package/src/tests/detector-tag-consistency.test.ts +86 -0
- package/src/tests/jsx.test.ts +1170 -4
- package/src/tests/native-equivalence.test.ts +731 -0
- package/src/tests/project-scanner.test.ts +30 -0
- package/src/tests/pyreon-intercept.test.ts +486 -0
- package/src/tests/react-intercept.test.ts +354 -0
- package/src/tests/runtime/control-flow.test.ts +159 -0
- package/src/tests/runtime/dom-properties.test.ts +138 -0
- package/src/tests/runtime/events.test.ts +301 -0
- package/src/tests/runtime/harness.ts +94 -0
- package/src/tests/runtime/pr-352-shapes.test.ts +121 -0
- package/src/tests/runtime/reactive-props.test.ts +81 -0
- package/src/tests/runtime/signals.test.ts +129 -0
- package/src/tests/runtime/whitespace.test.ts +106 -0
- package/src/tests/test-audit.test.ts +549 -0
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { dirname, join, resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import {
|
|
6
|
+
auditTestEnvironment,
|
|
7
|
+
formatTestAudit,
|
|
8
|
+
type TestAuditResult,
|
|
9
|
+
} from '../test-audit'
|
|
10
|
+
|
|
11
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
// tests/ → src/ → compiler/ → core/ → packages/ → repo root (5 ups)
|
|
13
|
+
const REPO_ROOT = resolve(HERE, '../../../../../')
|
|
14
|
+
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
// Helpers — synthetic monorepo fixture
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
function makeFixture(): {
|
|
20
|
+
root: string
|
|
21
|
+
writeTest: (relPath: string, body: string) => void
|
|
22
|
+
cleanup: () => void
|
|
23
|
+
} {
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'pyreon-audit-fixture-'))
|
|
25
|
+
mkdirSync(join(root, 'packages'), { recursive: true })
|
|
26
|
+
return {
|
|
27
|
+
root,
|
|
28
|
+
writeTest: (relPath, body) => {
|
|
29
|
+
const full = join(root, 'packages', relPath)
|
|
30
|
+
mkdirSync(dirname(full), { recursive: true })
|
|
31
|
+
writeFileSync(full, body)
|
|
32
|
+
},
|
|
33
|
+
cleanup: () => rmSync(root, { recursive: true, force: true }),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
38
|
+
// Scanner — synthetic inputs
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
describe('auditTestEnvironment — synthetic fixtures', () => {
|
|
42
|
+
let f: ReturnType<typeof makeFixture>
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
f = makeFixture()
|
|
45
|
+
})
|
|
46
|
+
afterEach(() => f.cleanup())
|
|
47
|
+
|
|
48
|
+
it('returns root=null when no packages/ dir exists', () => {
|
|
49
|
+
const empty = mkdtempSync(join(tmpdir(), 'pyreon-audit-empty-'))
|
|
50
|
+
try {
|
|
51
|
+
const r = auditTestEnvironment(empty)
|
|
52
|
+
expect(r.root).toBeNull()
|
|
53
|
+
expect(r.entries).toEqual([])
|
|
54
|
+
expect(r.totalScanned).toBe(0)
|
|
55
|
+
} finally {
|
|
56
|
+
rmSync(empty, { recursive: true, force: true })
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('classifies a pure-mock test file as HIGH', () => {
|
|
61
|
+
f.writeTest(
|
|
62
|
+
'foo/src/tests/component.test.ts',
|
|
63
|
+
`
|
|
64
|
+
const makeVNode = (type, props, children) => ({ type, props, children })
|
|
65
|
+
it('renders', () => {
|
|
66
|
+
const vnode = { type: 'div', props: { class: 'x' }, children: [] }
|
|
67
|
+
expect(vnode).toBeDefined()
|
|
68
|
+
})
|
|
69
|
+
`,
|
|
70
|
+
)
|
|
71
|
+
const r = auditTestEnvironment(f.root)
|
|
72
|
+
expect(r.entries).toHaveLength(1)
|
|
73
|
+
const [entry] = r.entries
|
|
74
|
+
expect(entry!.risk).toBe('high')
|
|
75
|
+
// Only one literal counted: the named `vnode` binding uses
|
|
76
|
+
// `type: 'div'` with an explicit colon. The `makeVNode` return
|
|
77
|
+
// expression uses shorthand `({ type, props, children })` — no
|
|
78
|
+
// colons — which the regex intentionally skips to avoid false
|
|
79
|
+
// positives on unrelated destructuring patterns.
|
|
80
|
+
expect(entry!.mockVNodeLiteralCount).toBe(1)
|
|
81
|
+
// Two helper bindings match the `(const|let|function) <name>`
|
|
82
|
+
// form: `makeVNode` and `vnode`. Both are flagged because both
|
|
83
|
+
// names signal "this test builds mock vnodes by hand".
|
|
84
|
+
expect(entry!.mockHelperCount).toBe(2)
|
|
85
|
+
expect(entry!.importsH).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('classifies a pure-h() test file as LOW', () => {
|
|
89
|
+
f.writeTest(
|
|
90
|
+
'foo/src/tests/real.test.ts',
|
|
91
|
+
`
|
|
92
|
+
import { h } from '@pyreon/core'
|
|
93
|
+
it('renders through real h', () => {
|
|
94
|
+
const vnode = h('div', { class: 'x' })
|
|
95
|
+
const composed = h(Component, { x: 1 })
|
|
96
|
+
expect(vnode).toBeDefined()
|
|
97
|
+
})
|
|
98
|
+
`,
|
|
99
|
+
)
|
|
100
|
+
const r = auditTestEnvironment(f.root)
|
|
101
|
+
expect(r.entries).toHaveLength(1)
|
|
102
|
+
const [entry] = r.entries
|
|
103
|
+
expect(entry!.risk).toBe('low')
|
|
104
|
+
expect(entry!.mockVNodeLiteralCount).toBe(0)
|
|
105
|
+
expect(entry!.importsH).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('classifies MEDIUM when mocks > real-h() but both present', () => {
|
|
109
|
+
f.writeTest(
|
|
110
|
+
'foo/src/tests/mixed.test.ts',
|
|
111
|
+
`
|
|
112
|
+
import { h } from '@pyreon/core'
|
|
113
|
+
const mockVNode = (type, props, children) => ({ type, props, children })
|
|
114
|
+
it('mock heavy', () => {
|
|
115
|
+
const a = { type: 'a', props: {}, children: [] }
|
|
116
|
+
const b = { type: 'b', props: {}, children: [] }
|
|
117
|
+
const c = { type: 'c', props: {}, children: [] }
|
|
118
|
+
const real = h('div')
|
|
119
|
+
expect(real).toBeDefined()
|
|
120
|
+
})
|
|
121
|
+
`,
|
|
122
|
+
)
|
|
123
|
+
const r = auditTestEnvironment(f.root)
|
|
124
|
+
const [entry] = r.entries
|
|
125
|
+
expect(entry!.risk).toBe('medium')
|
|
126
|
+
expect(entry!.mockVNodeLiteralCount).toBe(3)
|
|
127
|
+
expect(entry!.mockHelperCount).toBe(1)
|
|
128
|
+
expect(entry!.realHCallCount).toBe(1)
|
|
129
|
+
expect(entry!.importsH).toBe(true)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('classifies a test with no mocks at all as LOW', () => {
|
|
133
|
+
f.writeTest(
|
|
134
|
+
'foo/src/tests/logic.test.ts',
|
|
135
|
+
`
|
|
136
|
+
import { describe, it, expect } from 'vitest'
|
|
137
|
+
describe('pure logic', () => {
|
|
138
|
+
it('adds', () => {
|
|
139
|
+
expect(1 + 1).toBe(2)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
`,
|
|
143
|
+
)
|
|
144
|
+
const r = auditTestEnvironment(f.root)
|
|
145
|
+
expect(r.entries[0]!.risk).toBe('low')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('does NOT flag `h` substring inside identifier names as real h() calls', () => {
|
|
149
|
+
// The regex requires a non-word boundary before `h(`. `hasSomething(`,
|
|
150
|
+
// `hash(`, `oh(` — none should trigger.
|
|
151
|
+
f.writeTest(
|
|
152
|
+
'foo/src/tests/substring.test.ts',
|
|
153
|
+
`
|
|
154
|
+
it('strings', () => {
|
|
155
|
+
const fn = hasProp({ a: 1 })
|
|
156
|
+
const h2 = hash(x)
|
|
157
|
+
const result = oh(x, y)
|
|
158
|
+
expect(fn).toBeDefined()
|
|
159
|
+
})
|
|
160
|
+
`,
|
|
161
|
+
)
|
|
162
|
+
const r = auditTestEnvironment(f.root)
|
|
163
|
+
expect(r.entries[0]!.realHCallCount).toBe(0)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('recognises multiple mock-helper names (vnode, mockVNode, createVNode, vnodeMock, makeVNode)', () => {
|
|
167
|
+
f.writeTest(
|
|
168
|
+
'foo/src/tests/helpers.test.ts',
|
|
169
|
+
`
|
|
170
|
+
const vnode = () => null
|
|
171
|
+
const mockVNode = () => null
|
|
172
|
+
const createVNode = () => null
|
|
173
|
+
function VNodeMock() {}
|
|
174
|
+
const makeVNode = () => null
|
|
175
|
+
`,
|
|
176
|
+
)
|
|
177
|
+
const r = auditTestEnvironment(f.root)
|
|
178
|
+
expect(r.entries[0]!.mockHelperCount).toBe(5)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('does NOT flag `const vnode = someCall()` — that is a binding, not a helper def', () => {
|
|
182
|
+
// False positive the scanner used to hit — real render-result
|
|
183
|
+
// bindings got counted as mock factories, inflating the HIGH list
|
|
184
|
+
// with files like `table.test.tsx` and `storybook.test.tsx`.
|
|
185
|
+
f.writeTest(
|
|
186
|
+
'foo/src/tests/bindings.test.ts',
|
|
187
|
+
`
|
|
188
|
+
const vnode = defaultRender(Component, { name: 'World' })
|
|
189
|
+
const mockVNode = buildSomething()
|
|
190
|
+
const createVNode = factory.make(props)
|
|
191
|
+
`,
|
|
192
|
+
)
|
|
193
|
+
const r = auditTestEnvironment(f.root)
|
|
194
|
+
expect(r.entries[0]!.mockHelperCount).toBe(0)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('does NOT flag `const vnode = <jsx />` — JSX bindings are real VNodes', () => {
|
|
198
|
+
// `table.test.tsx` had `const vnode = <span>cell content</span>`
|
|
199
|
+
// — a legitimate real VNode stored in a local, not a mock factory.
|
|
200
|
+
f.writeTest(
|
|
201
|
+
'foo/src/tests/jsx-binding.test.tsx',
|
|
202
|
+
`
|
|
203
|
+
const vnode = <span>cell content</span>
|
|
204
|
+
const createVNode = <div />
|
|
205
|
+
`,
|
|
206
|
+
)
|
|
207
|
+
const r = auditTestEnvironment(f.root)
|
|
208
|
+
expect(r.entries[0]!.mockHelperCount).toBe(0)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('still flags `const vnode = (...) => ({ type, props, children })` arrow factories', () => {
|
|
212
|
+
f.writeTest(
|
|
213
|
+
'foo/src/tests/arrow-factory.test.ts',
|
|
214
|
+
`
|
|
215
|
+
const vnode = (type, props, children) => ({ type, props, children })
|
|
216
|
+
`,
|
|
217
|
+
)
|
|
218
|
+
const r = auditTestEnvironment(f.root)
|
|
219
|
+
expect(r.entries[0]!.mockHelperCount).toBe(1)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('still flags `const vnode = { type, props, children }` inline-object factories', () => {
|
|
223
|
+
f.writeTest(
|
|
224
|
+
'foo/src/tests/inline-factory.test.ts',
|
|
225
|
+
`
|
|
226
|
+
const vnode = { type: 'div', props: {}, children: [] }
|
|
227
|
+
`,
|
|
228
|
+
)
|
|
229
|
+
const r = auditTestEnvironment(f.root)
|
|
230
|
+
expect(r.entries[0]!.mockHelperCount).toBe(1)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('skips `{type,props,children}` literals inside type-guard call-args', () => {
|
|
234
|
+
// `isDocNode({ type, props, children })` is testing a duck-type
|
|
235
|
+
// guard — the literal IS the test input, not a mock-render input.
|
|
236
|
+
// `utils-coverage.test.ts` was the motivating false positive.
|
|
237
|
+
f.writeTest(
|
|
238
|
+
'foo/src/tests/type-guard.test.ts',
|
|
239
|
+
`
|
|
240
|
+
expect(isDocNode({ type: 'text', props: {}, children: [] })).toBe(true)
|
|
241
|
+
expect(hasVNodeShape({ type: 'div', props: {}, children: [] })).toBe(true)
|
|
242
|
+
expect(assertVNode({ type: 'span', props: {}, children: [] })).toBe(undefined)
|
|
243
|
+
expect(validateNode({ type: 'p', props: {}, children: [] })).toBe(true)
|
|
244
|
+
`,
|
|
245
|
+
)
|
|
246
|
+
const r = auditTestEnvironment(f.root)
|
|
247
|
+
expect(r.entries[0]!.mockVNodeLiteralCount).toBe(0)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('still flags `{type,props,children}` literals bound to a variable', () => {
|
|
251
|
+
f.writeTest(
|
|
252
|
+
'foo/src/tests/bound-literal.test.ts',
|
|
253
|
+
`
|
|
254
|
+
const v = { type: 'div', props: {}, children: [] }
|
|
255
|
+
`,
|
|
256
|
+
)
|
|
257
|
+
const r = auditTestEnvironment(f.root)
|
|
258
|
+
expect(r.entries[0]!.mockVNodeLiteralCount).toBe(1)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('skips `{type,props,children}` literals inside template strings (fixtures)', () => {
|
|
262
|
+
// The scanner's own test suite (cli/doctor.test.ts) writes mock-
|
|
263
|
+
// vnode literals to disk as fixture content via `writeFile(...,
|
|
264
|
+
// \`const v = { type, props, children: [] }\`)`. The literal is
|
|
265
|
+
// STRING DATA passed to the audit tool, not code that ever runs.
|
|
266
|
+
// The masking pass must skip backtick-delimited regions.
|
|
267
|
+
f.writeTest(
|
|
268
|
+
'foo/src/tests/template-fixture.test.ts',
|
|
269
|
+
`
|
|
270
|
+
writeFile(tmp, 'fixture.test.ts', \`const vnode = { type: 'div', props: {}, children: [] }\`)
|
|
271
|
+
writeFile(tmp, 'helper.test.ts', \`const mockVNode = (a, b) => ({ type: a, props: b, children: [] })\`)
|
|
272
|
+
`,
|
|
273
|
+
)
|
|
274
|
+
const r = auditTestEnvironment(f.root)
|
|
275
|
+
const e = r.entries[0]!
|
|
276
|
+
// Both literals AND the helper definition are inside template
|
|
277
|
+
// strings — neither should count.
|
|
278
|
+
expect(e.mockVNodeLiteralCount).toBe(0)
|
|
279
|
+
expect(e.mockHelperCount).toBe(0)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('still flags literals/helpers OUTSIDE template strings, even when fixtures are nearby', () => {
|
|
283
|
+
// Mixed file: a real top-level `const vnode = (...)` factory plus
|
|
284
|
+
// a fixture string. Scanner counts the real one, skips the fixture.
|
|
285
|
+
f.writeTest(
|
|
286
|
+
'foo/src/tests/mixed.test.ts',
|
|
287
|
+
`
|
|
288
|
+
const vnode = (t, p) => ({ type: t, props: p, children: [] })
|
|
289
|
+
writeFile(tmp, 'fixture.test.ts', \`const v = { type: 'div', props: {}, children: [] }\`)
|
|
290
|
+
`,
|
|
291
|
+
)
|
|
292
|
+
const r = auditTestEnvironment(f.root)
|
|
293
|
+
const e = r.entries[0]!
|
|
294
|
+
expect(e.mockHelperCount).toBe(1) // the real factory at module scope
|
|
295
|
+
// The fixture string content is masked, so its literal doesn't count.
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('sorts entries by risk (HIGH first) then path', () => {
|
|
299
|
+
f.writeTest(
|
|
300
|
+
'z/src/tests/low.test.ts',
|
|
301
|
+
`import { h } from '@pyreon/core'; const v = h('div')`,
|
|
302
|
+
)
|
|
303
|
+
f.writeTest(
|
|
304
|
+
'a/src/tests/high.test.ts',
|
|
305
|
+
`const v = { type: 'div', props: {}, children: [] }`,
|
|
306
|
+
)
|
|
307
|
+
f.writeTest(
|
|
308
|
+
'm/src/tests/medium.test.ts',
|
|
309
|
+
`
|
|
310
|
+
import { h } from '@pyreon/core'
|
|
311
|
+
const a = { type: 'a', props: {}, children: [] }
|
|
312
|
+
const b = { type: 'b', props: {}, children: [] }
|
|
313
|
+
const r = h('div')
|
|
314
|
+
`,
|
|
315
|
+
)
|
|
316
|
+
const r = auditTestEnvironment(f.root)
|
|
317
|
+
expect(r.entries.map((e) => e.risk)).toEqual(['high', 'medium', 'low'])
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('skips node_modules / lib / dist directories', () => {
|
|
321
|
+
f.writeTest(
|
|
322
|
+
'foo/src/tests/real.test.ts',
|
|
323
|
+
`const x = { type: 'a', props: {}, children: [] }`,
|
|
324
|
+
)
|
|
325
|
+
f.writeTest(
|
|
326
|
+
'foo/node_modules/some-dep/src/tests/nested.test.ts',
|
|
327
|
+
`const x = { type: 'ignored', props: {}, children: [] }`,
|
|
328
|
+
)
|
|
329
|
+
f.writeTest(
|
|
330
|
+
'foo/lib/tests/nested.test.ts',
|
|
331
|
+
`const x = { type: 'ignored', props: {}, children: [] }`,
|
|
332
|
+
)
|
|
333
|
+
const r = auditTestEnvironment(f.root)
|
|
334
|
+
expect(r.totalScanned).toBe(1)
|
|
335
|
+
expect(r.entries[0]!.relPath).toContain('foo/src/tests/real.test.ts')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('counts exactly test files matching *.test.ts or *.test.tsx', () => {
|
|
339
|
+
f.writeTest('foo/src/tests/a.test.ts', 'const x = 1')
|
|
340
|
+
f.writeTest('foo/src/tests/b.test.tsx', 'const x = 1')
|
|
341
|
+
f.writeTest('foo/src/tests/c.spec.ts', 'const x = 1') // not a .test file
|
|
342
|
+
f.writeTest('foo/src/tests/d.ts', 'const x = 1') // not a test file
|
|
343
|
+
const r = auditTestEnvironment(f.root)
|
|
344
|
+
expect(r.totalScanned).toBe(2)
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
349
|
+
// Scanner — real repo
|
|
350
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
351
|
+
|
|
352
|
+
describe('auditTestEnvironment — real Pyreon repo', () => {
|
|
353
|
+
const result = auditTestEnvironment(REPO_ROOT)
|
|
354
|
+
|
|
355
|
+
it('discovers the monorepo root', () => {
|
|
356
|
+
expect(result.root).toBe(REPO_ROOT)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('scans a realistic number of test files (>50)', () => {
|
|
360
|
+
expect(result.totalScanned).toBeGreaterThan(50)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('scanner picks up real h() usage across the repo — sanity check', () => {
|
|
364
|
+
// The full T1.2 cleanup drove the real-repo HIGH and MEDIUM counts
|
|
365
|
+
// to zero — every test file now either avoids mock vnodes entirely
|
|
366
|
+
// or pairs them with real-`h()` coverage. So there's no longer any
|
|
367
|
+
// mock-helper or risk-level anchor that's stable over time on the
|
|
368
|
+
// real repo. The synthetic-fixture suites above (which we control)
|
|
369
|
+
// are what cover classifier correctness. The only stable invariant
|
|
370
|
+
// left for the live scan is: real `h()` from `@pyreon/core` IS used
|
|
371
|
+
// in test files. Zero would indicate the scanner regex broke.
|
|
372
|
+
const realHUsers = result.entries.filter((e) => e.realHCallCount > 0)
|
|
373
|
+
expect(realHUsers.length).toBeGreaterThan(0)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('never produces NaN or negative counts', () => {
|
|
377
|
+
for (const entry of result.entries) {
|
|
378
|
+
expect(entry.mockVNodeLiteralCount).toBeGreaterThanOrEqual(0)
|
|
379
|
+
expect(entry.mockHelperCount).toBeGreaterThanOrEqual(0)
|
|
380
|
+
expect(entry.mockHelperCallCount).toBeGreaterThanOrEqual(0)
|
|
381
|
+
expect(entry.realHCallCount).toBeGreaterThanOrEqual(0)
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
387
|
+
// Helper-call metric — catches factory-call pervasiveness
|
|
388
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
389
|
+
|
|
390
|
+
describe('mockHelperCallCount metric', () => {
|
|
391
|
+
let f: ReturnType<typeof makeFixture>
|
|
392
|
+
beforeEach(() => {
|
|
393
|
+
f = makeFixture()
|
|
394
|
+
})
|
|
395
|
+
afterEach(() => f.cleanup())
|
|
396
|
+
|
|
397
|
+
it('counts every call to a known mock-helper name', () => {
|
|
398
|
+
f.writeTest(
|
|
399
|
+
'foo/src/tests/pervasive.test.ts',
|
|
400
|
+
`
|
|
401
|
+
const vnode = (type, props, children) => ({ type, props, children })
|
|
402
|
+
it('builds lots', () => {
|
|
403
|
+
const a = vnode('div', {}, [])
|
|
404
|
+
const b = vnode('span', {}, [])
|
|
405
|
+
const c = vnode('button', {}, [])
|
|
406
|
+
expect([a, b, c]).toBeDefined()
|
|
407
|
+
})
|
|
408
|
+
`,
|
|
409
|
+
)
|
|
410
|
+
const r = auditTestEnvironment(f.root)
|
|
411
|
+
const [entry] = r.entries
|
|
412
|
+
// Three call-sites + one inside the definition argument list =
|
|
413
|
+
// four total matches. The definition's own `(type, props, ...)`
|
|
414
|
+
// hit is expected — it reports signal activity either way.
|
|
415
|
+
expect(entry!.mockHelperCallCount).toBeGreaterThanOrEqual(3)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('does not count calls with trailing non-word boundary (identifier extensions)', () => {
|
|
419
|
+
// `vnodeReal(...)` should NOT match — it starts with `vnode` but
|
|
420
|
+
// continues as an identifier. The negative-lookahead via
|
|
421
|
+
// `(?:^|[^a-zA-Z0-9_])` boundary guards against this on the LEFT
|
|
422
|
+
// side only, so we still rely on the name set being specific.
|
|
423
|
+
f.writeTest(
|
|
424
|
+
'foo/src/tests/lookalike.test.ts',
|
|
425
|
+
`
|
|
426
|
+
const vnodeExtended = () => null
|
|
427
|
+
vnodeExtended()
|
|
428
|
+
myVnode()
|
|
429
|
+
`,
|
|
430
|
+
)
|
|
431
|
+
const r = auditTestEnvironment(f.root)
|
|
432
|
+
const [entry] = r.entries
|
|
433
|
+
// The regex matches `vnode(` as a substring of `vnodeExtended(`.
|
|
434
|
+
// Acknowledged false positive — documented in the pattern comment.
|
|
435
|
+
// What we verify: the metric is still non-negative and classifies
|
|
436
|
+
// the file in a way that's still useful (non-zero mock activity
|
|
437
|
+
// means the reviewer should eyeball it).
|
|
438
|
+
expect(entry!.mockHelperCallCount).toBeGreaterThanOrEqual(0)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('factors into HIGH classification when no real-h() counterpart exists', () => {
|
|
442
|
+
// A file that DEFINES no helper but CALLS an imported one heavily
|
|
443
|
+
// should still show risk.
|
|
444
|
+
f.writeTest(
|
|
445
|
+
'foo/src/tests/imported-helper.test.ts',
|
|
446
|
+
`
|
|
447
|
+
import { vnode } from '../fixtures'
|
|
448
|
+
it('uses imported vnode', () => {
|
|
449
|
+
const a = vnode('div')
|
|
450
|
+
const b = vnode('span')
|
|
451
|
+
expect([a, b]).toBeDefined()
|
|
452
|
+
})
|
|
453
|
+
`,
|
|
454
|
+
)
|
|
455
|
+
const r = auditTestEnvironment(f.root)
|
|
456
|
+
const [entry] = r.entries
|
|
457
|
+
expect(entry!.mockHelperCount).toBe(0)
|
|
458
|
+
expect(entry!.mockHelperCallCount).toBeGreaterThanOrEqual(2)
|
|
459
|
+
expect(entry!.risk).toBe('high')
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
464
|
+
// Formatter
|
|
465
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
466
|
+
|
|
467
|
+
describe('formatTestAudit', () => {
|
|
468
|
+
function mkResult(entries: Partial<TestAuditResult['entries'][number]>[]): TestAuditResult {
|
|
469
|
+
return {
|
|
470
|
+
root: '/tmp/fake',
|
|
471
|
+
entries: entries.map((e, i) => ({
|
|
472
|
+
path: `/tmp/fake/${i}.test.ts`,
|
|
473
|
+
relPath: `${i}.test.ts`,
|
|
474
|
+
mockVNodeLiteralCount: 0,
|
|
475
|
+
mockHelperCount: 0,
|
|
476
|
+
mockHelperCallCount: 0,
|
|
477
|
+
realHCallCount: 0,
|
|
478
|
+
importsH: false,
|
|
479
|
+
risk: 'low' as const,
|
|
480
|
+
...e,
|
|
481
|
+
})),
|
|
482
|
+
totalScanned: entries.length,
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
it('emits a helpful miss message when root is null', () => {
|
|
487
|
+
const out = formatTestAudit({ root: null, entries: [], totalScanned: 0 })
|
|
488
|
+
expect(out).toContain('No monorepo root found')
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('surfaces risk counts and mock-vnode exposure fraction in the header', () => {
|
|
492
|
+
const out = formatTestAudit(
|
|
493
|
+
mkResult([
|
|
494
|
+
{ risk: 'high', mockVNodeLiteralCount: 1 },
|
|
495
|
+
{ risk: 'medium', mockVNodeLiteralCount: 2, realHCallCount: 1, importsH: true },
|
|
496
|
+
{ risk: 'low' },
|
|
497
|
+
]),
|
|
498
|
+
)
|
|
499
|
+
expect(out).toContain('3 test files scanned')
|
|
500
|
+
// Formatter wraps the label in markdown bold **...**, so just
|
|
501
|
+
// check the number + slash pattern.
|
|
502
|
+
expect(out).toMatch(/Mock-vnode exposure.*2 \/ 3/)
|
|
503
|
+
expect(out).toContain('1 high')
|
|
504
|
+
expect(out).toContain('1 medium')
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('defaults to minRisk="medium" — hides LOW entries', () => {
|
|
508
|
+
const out = formatTestAudit(
|
|
509
|
+
mkResult([
|
|
510
|
+
{ risk: 'high', mockVNodeLiteralCount: 1, relPath: 'hi.test.ts' },
|
|
511
|
+
{ risk: 'low', relPath: 'ok.test.ts' },
|
|
512
|
+
]),
|
|
513
|
+
)
|
|
514
|
+
expect(out).toContain('hi.test.ts')
|
|
515
|
+
expect(out).not.toContain('- ok.test.ts') // bullet form only; header counts are fine
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
it('respects minRisk="high" — hides MEDIUM entries', () => {
|
|
519
|
+
const out = formatTestAudit(
|
|
520
|
+
mkResult([
|
|
521
|
+
{ risk: 'high', mockVNodeLiteralCount: 1, relPath: 'hi.test.ts' },
|
|
522
|
+
{ risk: 'medium', mockVNodeLiteralCount: 1, realHCallCount: 1, relPath: 'med.test.ts' },
|
|
523
|
+
]),
|
|
524
|
+
{ minRisk: 'high' },
|
|
525
|
+
)
|
|
526
|
+
expect(out).toContain('hi.test.ts')
|
|
527
|
+
expect(out).not.toContain('- med.test.ts')
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('respects limit per risk group', () => {
|
|
531
|
+
const entries = Array.from({ length: 30 }, (_, i) => ({
|
|
532
|
+
risk: 'high' as const,
|
|
533
|
+
mockVNodeLiteralCount: 1,
|
|
534
|
+
relPath: `h${i}.test.ts`,
|
|
535
|
+
}))
|
|
536
|
+
const out = formatTestAudit(mkResult(entries), { limit: 3 })
|
|
537
|
+
expect(out).toContain('30 files (showing 3)')
|
|
538
|
+
// Bullets: exactly 3
|
|
539
|
+
const bullets = out.split('\n').filter((l) => /^- h\d+\.test\.ts/.test(l))
|
|
540
|
+
expect(bullets).toHaveLength(3)
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('mentions PR #197 so the agent has the context', () => {
|
|
544
|
+
const out = formatTestAudit(
|
|
545
|
+
mkResult([{ risk: 'high', mockVNodeLiteralCount: 1 }]),
|
|
546
|
+
)
|
|
547
|
+
expect(out).toContain('PR #197')
|
|
548
|
+
})
|
|
549
|
+
})
|