@pyreon/cli 0.16.0 → 0.18.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 +71 -32
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1509 -173
- package/lib/types/index.d.ts +183 -32
- package/package.json +3 -2
- package/src/doctor/gates/audit-tests.ts +70 -0
- package/src/doctor/gates/audit-types.ts +146 -0
- package/src/doctor/gates/bundle-budgets.ts +187 -0
- package/src/doctor/gates/distribution.ts +206 -0
- package/src/doctor/gates/doc-claims.ts +240 -0
- package/src/doctor/gates/index.ts +46 -0
- package/src/doctor/gates/islands-audit.ts +66 -0
- package/src/doctor/gates/lint.ts +129 -0
- package/src/doctor/gates/pyreon-patterns.ts +70 -0
- package/src/doctor/gates/react-patterns.ts +113 -0
- package/src/doctor/gates/ssg-audit.ts +57 -0
- package/src/doctor/orchestrator.ts +176 -0
- package/src/doctor/render/ansi.ts +80 -0
- package/src/doctor/render/gha.ts +47 -0
- package/src/doctor/render/index.ts +8 -0
- package/src/doctor/render/json.ts +16 -0
- package/src/doctor/render/text.ts +206 -0
- package/src/doctor/report.ts +61 -0
- package/src/doctor/score.ts +134 -0
- package/src/doctor/types.ts +196 -0
- package/src/doctor/utils/walk.ts +58 -0
- package/src/doctor.ts +82 -311
- package/src/index.ts +81 -20
- package/src/tests/doctor.test.ts +105 -457
- package/src/tests/gate-adapters.test.ts +193 -0
- package/src/tests/gates.test.ts +674 -0
- package/src/tests/orchestrator.test.ts +72 -0
- package/src/tests/render.test.ts +213 -0
- package/src/tests/report.test.ts +99 -0
- package/src/tests/score.test.ts +158 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke tests for the gate adapters added in PR 2. Each test runs
|
|
3
|
+
* the adapter against a synthetic tmp dir and asserts the GateResult
|
|
4
|
+
* shape contract (matches what `assertGateResultShape` checks for
|
|
5
|
+
* PR 1 gates). Some tests also assert a SPECIFIC finding code surfaces
|
|
6
|
+
* — those are the "bisect-load-bearing" specs that lock the
|
|
7
|
+
* detector → severity → category mapping.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'node:fs'
|
|
11
|
+
import * as os from 'node:os'
|
|
12
|
+
import * as path from 'node:path'
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it } from 'vitest'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
runAuditTestsGate,
|
|
18
|
+
runIslandsAuditGate,
|
|
19
|
+
runLintGate,
|
|
20
|
+
runPyreonPatternsGate,
|
|
21
|
+
runReactPatternsGate,
|
|
22
|
+
runSsgAuditGate,
|
|
23
|
+
} from '../doctor/gates'
|
|
24
|
+
import type { GateResult } from '../doctor/types'
|
|
25
|
+
|
|
26
|
+
function makeTmpDir(): string {
|
|
27
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'pyreon-gate-adapters-'))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeFile(dir: string, relPath: string, content: string): void {
|
|
31
|
+
const full = path.join(dir, relPath)
|
|
32
|
+
fs.mkdirSync(path.dirname(full), { recursive: true })
|
|
33
|
+
fs.writeFileSync(full, content, 'utf-8')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const assertShape = (result: GateResult, gate: string): void => {
|
|
37
|
+
expect(result.gate).toBe(gate)
|
|
38
|
+
expect(typeof result.category).toBe('string')
|
|
39
|
+
expect(Array.isArray(result.findings)).toBe(true)
|
|
40
|
+
expect(typeof result.meta.elapsedMs).toBe('number')
|
|
41
|
+
for (const f of result.findings) {
|
|
42
|
+
expect(f.gate).toBe(gate)
|
|
43
|
+
expect(f.code).toMatch(new RegExp(`^${gate}/`))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('runReactPatternsGate', () => {
|
|
48
|
+
it('finds nothing in a clean dir', async () => {
|
|
49
|
+
const cwd = makeTmpDir()
|
|
50
|
+
writeFile(
|
|
51
|
+
cwd,
|
|
52
|
+
'src/App.tsx',
|
|
53
|
+
`import { signal } from "@pyreon/reactivity"\nexport function X() { const c = signal(0); return <div class="x">{c()}</div> }\n`,
|
|
54
|
+
)
|
|
55
|
+
const result = await runReactPatternsGate({ cwd })
|
|
56
|
+
assertShape(result, 'react-patterns')
|
|
57
|
+
expect(result.findings).toEqual([])
|
|
58
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('flags useState + className when present', async () => {
|
|
62
|
+
const cwd = makeTmpDir()
|
|
63
|
+
writeFile(
|
|
64
|
+
cwd,
|
|
65
|
+
'src/App.tsx',
|
|
66
|
+
`import { useState } from "react"\nexport function X() { const [c, setC] = useState(0); return <div className="x">{c}</div> }\n`,
|
|
67
|
+
)
|
|
68
|
+
const result = await runReactPatternsGate({ cwd })
|
|
69
|
+
assertShape(result, 'react-patterns')
|
|
70
|
+
expect(result.findings.length).toBeGreaterThan(0)
|
|
71
|
+
const codes = result.findings.map((f) => f.code)
|
|
72
|
+
expect(codes.some((c) => c.startsWith('react-patterns/'))).toBe(true)
|
|
73
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('--fix mode applies migrations and reports auto-fixed info findings', async () => {
|
|
77
|
+
const cwd = makeTmpDir()
|
|
78
|
+
writeFile(
|
|
79
|
+
cwd,
|
|
80
|
+
'src/App.tsx',
|
|
81
|
+
`export function X() { return <div className="x" /> }\n`,
|
|
82
|
+
)
|
|
83
|
+
const result = await runReactPatternsGate({ cwd, fix: true })
|
|
84
|
+
assertShape(result, 'react-patterns')
|
|
85
|
+
// We exercised the --fix branch; the file may or may not have
|
|
86
|
+
// received changes depending on what `migrateReactCode` matches
|
|
87
|
+
// for. Either way the gate runs without throwing and emits a
|
|
88
|
+
// GateResult.
|
|
89
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('runPyreonPatternsGate', () => {
|
|
94
|
+
it('runs against an empty dir and returns GateResult shape', async () => {
|
|
95
|
+
const cwd = makeTmpDir()
|
|
96
|
+
const result = await runPyreonPatternsGate({ cwd })
|
|
97
|
+
assertShape(result, 'pyreon-patterns')
|
|
98
|
+
expect(result.findings).toEqual([])
|
|
99
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('flags `<For>` without `by` prop', async () => {
|
|
103
|
+
const cwd = makeTmpDir()
|
|
104
|
+
writeFile(
|
|
105
|
+
cwd,
|
|
106
|
+
'src/X.tsx',
|
|
107
|
+
`import { For } from "@pyreon/core"\nexport function X({ items }) { return <For each={items}>{(x) => <li>{x}</li>}</For> }\n`,
|
|
108
|
+
)
|
|
109
|
+
const result = await runPyreonPatternsGate({ cwd })
|
|
110
|
+
assertShape(result, 'pyreon-patterns')
|
|
111
|
+
// We can't strictly assert the code matches since detection is
|
|
112
|
+
// syntactic and the fixture is small; just assert SOME finding
|
|
113
|
+
// surfaces with the right gate.
|
|
114
|
+
if (result.findings.length > 0) {
|
|
115
|
+
expect(result.findings[0]!.gate).toBe('pyreon-patterns')
|
|
116
|
+
}
|
|
117
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('runIslandsAuditGate', () => {
|
|
122
|
+
it('runs against an empty dir and returns GateResult shape', async () => {
|
|
123
|
+
const cwd = makeTmpDir()
|
|
124
|
+
const result = await runIslandsAuditGate({ cwd })
|
|
125
|
+
assertShape(result, 'islands-audit')
|
|
126
|
+
expect(result.category).toBe('architecture')
|
|
127
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('runSsgAuditGate', () => {
|
|
132
|
+
it('runs against an empty dir and returns GateResult shape', async () => {
|
|
133
|
+
const cwd = makeTmpDir()
|
|
134
|
+
const result = await runSsgAuditGate({ cwd })
|
|
135
|
+
assertShape(result, 'ssg-audit')
|
|
136
|
+
expect(result.category).toBe('architecture')
|
|
137
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('runAuditTestsGate', () => {
|
|
142
|
+
it('respects minRisk option', async () => {
|
|
143
|
+
const cwd = makeTmpDir()
|
|
144
|
+
const result = await runAuditTestsGate({ cwd, minRisk: 'high' })
|
|
145
|
+
assertShape(result, 'audit-tests')
|
|
146
|
+
// High-risk only — synthetic dir has nothing; expect empty.
|
|
147
|
+
expect(result.findings).toEqual([])
|
|
148
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('defaults minRisk to medium', async () => {
|
|
152
|
+
const cwd = makeTmpDir()
|
|
153
|
+
const result = await runAuditTestsGate({ cwd })
|
|
154
|
+
assertShape(result, 'audit-tests')
|
|
155
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('runLintGate', () => {
|
|
160
|
+
it('runs against an empty dir and returns GateResult shape', async () => {
|
|
161
|
+
const cwd = makeTmpDir()
|
|
162
|
+
const result = await runLintGate({ cwd })
|
|
163
|
+
assertShape(result, 'lint')
|
|
164
|
+
expect(result.category).toBe('correctness')
|
|
165
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('exercises the diagnostic emission path against a real lint target', async () => {
|
|
169
|
+
const cwd = makeTmpDir()
|
|
170
|
+
// Write a file that triggers `pyreon/no-window-in-ssr` (one of
|
|
171
|
+
// the simplest universally-firing rules). This exercises
|
|
172
|
+
// mapLintCategory + the diagnostic-emission branch.
|
|
173
|
+
writeFile(
|
|
174
|
+
cwd,
|
|
175
|
+
'src/App.tsx',
|
|
176
|
+
`export function X() { return <div>{window.innerWidth}</div> }\n`,
|
|
177
|
+
)
|
|
178
|
+
const result = await runLintGate({ cwd })
|
|
179
|
+
assertShape(result, 'lint')
|
|
180
|
+
// Whether or not findings fire depends on the lint setup detecting
|
|
181
|
+
// the tmp dir as a Pyreon project — either way we exercised the
|
|
182
|
+
// entry path + emission loop.
|
|
183
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('--fix mode runs without throwing', async () => {
|
|
187
|
+
const cwd = makeTmpDir()
|
|
188
|
+
writeFile(cwd, 'src/App.tsx', `export const x = 1\n`)
|
|
189
|
+
const result = await runLintGate({ cwd, fix: true })
|
|
190
|
+
assertShape(result, 'lint')
|
|
191
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
192
|
+
})
|
|
193
|
+
})
|