@ranger1/dx 0.1.94 → 0.1.96
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/lib/cli/commands/core.js +76 -25
- package/lib/cli/flags.js +5 -1
- package/lib/cli/help.js +5 -4
- package/lib/codex-initial.js +3 -0
- package/package.json +1 -1
- package/skills/error-handling-audit-fixer/SKILL.md +71 -34
- package/skills/error-handling-audit-fixer/scripts/error_handling_audit.py +97 -39
- package/skills/git-pr-ship/SKILL.md +43 -1
package/lib/cli/commands/core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { join, relative } from 'node:path'
|
|
2
|
+
import { dirname, join, relative } from 'node:path'
|
|
3
3
|
import { logger } from '../../logger.js'
|
|
4
4
|
import { confirmManager } from '../../confirm.js'
|
|
5
5
|
import { execManager } from '../../exec.js'
|
|
@@ -83,11 +83,7 @@ export async function handleTest(cli, args) {
|
|
|
83
83
|
|
|
84
84
|
// 解析 -t 参数用于指定特定测试用例(使用原始参数列表)
|
|
85
85
|
const allArgs = cli.args // 使用原始参数列表包含所有标志
|
|
86
|
-
const
|
|
87
|
-
let testNamePattern = null
|
|
88
|
-
if (testNamePatternIndex !== -1 && testNamePatternIndex + 1 < allArgs.length) {
|
|
89
|
-
testNamePattern = allArgs[testNamePatternIndex + 1]
|
|
90
|
-
}
|
|
86
|
+
const testNamePattern = resolveTestNamePattern(allArgs)
|
|
91
87
|
|
|
92
88
|
// 根据测试类型自动设置环境标志
|
|
93
89
|
if (type === 'e2e' && !cli.flags.e2e) {
|
|
@@ -120,7 +116,8 @@ export async function handleTest(cli, args) {
|
|
|
120
116
|
process.exit(1)
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
|
|
119
|
+
const normalizedTestPath = normalizeE2eTestPathForCommand(cli, fileCommand, testPath)
|
|
120
|
+
let command = fileCommand.replace('{TEST_PATH}', shellEscape(normalizedTestPath))
|
|
124
121
|
|
|
125
122
|
if (testNamePattern) {
|
|
126
123
|
command += ` -t ${shellEscape(testNamePattern)}`
|
|
@@ -179,6 +176,15 @@ function shellEscape(value) {
|
|
|
179
176
|
return `'${String(value).replace(/'/g, `'\\''`)}'`
|
|
180
177
|
}
|
|
181
178
|
|
|
179
|
+
function resolveTestNamePattern(args = []) {
|
|
180
|
+
const aliases = ['-t', '--name', '--test-name-pattern']
|
|
181
|
+
for (let i = 0; i < args.length; i++) {
|
|
182
|
+
if (!aliases.includes(args[i])) continue
|
|
183
|
+
if (i + 1 < args.length) return args[i + 1]
|
|
184
|
+
}
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
|
|
182
188
|
function shouldUseDirectPathArg(command) {
|
|
183
189
|
const text = String(command || '')
|
|
184
190
|
return (
|
|
@@ -196,11 +202,11 @@ function normalizeUnitTestPathForCommand(cli, command, testPath) {
|
|
|
196
202
|
return rawPath
|
|
197
203
|
}
|
|
198
204
|
|
|
199
|
-
const
|
|
200
|
-
if (!
|
|
205
|
+
const projectCwd = resolveNxTargetProjectCwd(cli, command, ['test'])
|
|
206
|
+
if (!projectCwd) return rawPath
|
|
201
207
|
|
|
202
208
|
const projectRoot = cli?.projectRoot || process.cwd()
|
|
203
|
-
const absoluteProjectCwd = join(projectRoot,
|
|
209
|
+
const absoluteProjectCwd = join(projectRoot, projectCwd)
|
|
204
210
|
const absoluteTestPath = join(projectRoot, rawPath)
|
|
205
211
|
const relativePath = relative(absoluteProjectCwd, absoluteTestPath)
|
|
206
212
|
|
|
@@ -211,33 +217,78 @@ function normalizeUnitTestPathForCommand(cli, command, testPath) {
|
|
|
211
217
|
return relativePath
|
|
212
218
|
}
|
|
213
219
|
|
|
214
|
-
function
|
|
220
|
+
function normalizeE2eTestPathForCommand(cli, command, testPath) {
|
|
221
|
+
const rawPath = String(testPath || '')
|
|
222
|
+
if (!rawPath) return rawPath
|
|
223
|
+
|
|
224
|
+
const projectCwd = resolveNxTargetProjectCwd(cli, command, ['test:e2e'])
|
|
225
|
+
if (!projectCwd) return rawPath
|
|
226
|
+
|
|
215
227
|
const projectRoot = cli?.projectRoot || process.cwd()
|
|
216
|
-
const
|
|
217
|
-
|
|
228
|
+
const absoluteProjectCwd = join(projectRoot, projectCwd)
|
|
229
|
+
const absoluteTestPath = join(projectRoot, rawPath)
|
|
230
|
+
const relativePath = relative(absoluteProjectCwd, absoluteTestPath)
|
|
231
|
+
|
|
232
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
233
|
+
return rawPath
|
|
234
|
+
}
|
|
218
235
|
|
|
219
|
-
|
|
236
|
+
return relativePath
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveNxTargetProjectCwd(cli, command, targetNames = []) {
|
|
240
|
+
const projectRoot = cli?.projectRoot || process.cwd()
|
|
241
|
+
const nxResolution = extractNxTarget(command, targetNames)
|
|
242
|
+
if (!nxResolution) return null
|
|
243
|
+
|
|
244
|
+
const projectDir = join(projectRoot, 'apps', nxResolution.project)
|
|
245
|
+
const projectConfigPath = join(projectDir, 'project.json')
|
|
220
246
|
if (!existsSync(projectConfigPath)) return null
|
|
221
247
|
|
|
222
248
|
try {
|
|
223
249
|
const projectConfig = JSON.parse(readFileSync(projectConfigPath, 'utf8'))
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
250
|
+
const resolvedTarget = projectConfig?.targets?.[nxResolution.target]
|
|
251
|
+
const cwd = resolvedTarget?.options?.cwd
|
|
252
|
+
if (typeof cwd === 'string' && cwd.trim().length > 0) {
|
|
253
|
+
return cwd
|
|
254
|
+
}
|
|
255
|
+
if (nxResolution.target === 'test:e2e') {
|
|
256
|
+
const e2eDir = join(projectDir, 'e2e')
|
|
257
|
+
if (existsSync(e2eDir)) {
|
|
258
|
+
return relative(projectRoot, e2eDir)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return relative(projectRoot, dirname(projectConfigPath))
|
|
230
262
|
} catch {
|
|
231
263
|
return null
|
|
232
264
|
}
|
|
233
265
|
}
|
|
234
266
|
|
|
235
|
-
function
|
|
267
|
+
function extractNxTarget(command, targetNames = []) {
|
|
236
268
|
const text = String(command || '').trim()
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
269
|
+
const names = Array.isArray(targetNames) && targetNames.length > 0 ? targetNames : ['test']
|
|
270
|
+
|
|
271
|
+
for (const targetName of names) {
|
|
272
|
+
if (targetName === 'test') {
|
|
273
|
+
const directMatch = text.match(/\bnx(?:\.js)?\s+test\s+([^\s]+)/)
|
|
274
|
+
if (directMatch?.[1]) {
|
|
275
|
+
return { project: directMatch[1], target: 'test' }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const escapedTarget = targetName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
|
|
280
|
+
const runMatch = text.match(new RegExp(`\\bnx(?:\\.js)?\\s+run\\s+([^:\\s]+):${escapedTarget}\\b`))
|
|
281
|
+
if (runMatch?.[1]) {
|
|
282
|
+
return { project: runMatch[1], target: targetName }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const directColonMatch = text.match(new RegExp(`\\bnx(?:\\.js)?\\s+${escapedTarget}\\s+([^\\s]+)`))
|
|
286
|
+
if (directColonMatch?.[1]) {
|
|
287
|
+
return { project: directColonMatch[1], target: targetName }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null
|
|
241
292
|
}
|
|
242
293
|
|
|
243
294
|
export async function handleLint(cli, args) {
|
package/lib/cli/flags.js
CHANGED
|
@@ -24,7 +24,11 @@ export const FLAG_DEFINITIONS = {
|
|
|
24
24
|
{ flag: '--name', expectsValue: true },
|
|
25
25
|
{ flag: '-n', expectsValue: true },
|
|
26
26
|
],
|
|
27
|
-
test: [
|
|
27
|
+
test: [
|
|
28
|
+
{ flag: '-t', expectsValue: true },
|
|
29
|
+
{ flag: '--name', expectsValue: true },
|
|
30
|
+
{ flag: '--test-name-pattern', expectsValue: true },
|
|
31
|
+
],
|
|
28
32
|
package: [
|
|
29
33
|
{ flag: '--skip-build' },
|
|
30
34
|
{ flag: '--keep-workdir' },
|
package/lib/cli/help.js
CHANGED
|
@@ -41,11 +41,11 @@ export function showHelp() {
|
|
|
41
41
|
' dx db script fix-pending-transfer-status --prod # 运行数据库脚本(生产环境,需确认)',
|
|
42
42
|
' dx db script my-script --dev -- --arg1 --arg2 # 向脚本传递额外参数(-- 后面的部分)',
|
|
43
43
|
'',
|
|
44
|
-
' test [type] [target] [path] [-t pattern] 运行测试',
|
|
44
|
+
' test [type] [target] [path] [-t pattern|--name pattern] 运行测试',
|
|
45
45
|
' type: e2e, unit (默认: e2e)',
|
|
46
46
|
' target: 由 commands.json 的 test.<type>.<target> 决定(e2e 默认会拒绝隐式 all)',
|
|
47
47
|
' path: 测试文件或目录路径 (guarded e2e target 必填,例如 backend/quantify)',
|
|
48
|
-
' -t pattern: 指定测试用例名称模式 (可选,需要和 path 一起使用)',
|
|
48
|
+
' -t pattern / --name pattern: 指定测试用例名称模式 (可选,需要和 path 一起使用)',
|
|
49
49
|
' 说明: guarded E2E target 禁止无路径或 all 全量执行,dx test e2e all 也不受支持',
|
|
50
50
|
'',
|
|
51
51
|
' worktree [action] [num...] Git Worktree管理',
|
|
@@ -94,6 +94,7 @@ export function showHelp() {
|
|
|
94
94
|
' dx test e2e backend apps/backend/e2e/auth # 按目录运行后端 E2E',
|
|
95
95
|
' dx test e2e backend apps/backend/e2e/activity/activity.admin.e2e-spec.ts # 运行单个E2E测试文件',
|
|
96
96
|
' dx test e2e backend apps/backend/e2e/activity/activity.admin.e2e-spec.ts -t "should list all activity definitions" # 运行特定测试用例',
|
|
97
|
+
' dx test unit backend apps/backend/src/modules/chat/chat.service.spec.ts --name "should create chat" # 运行单测中的特定用例',
|
|
97
98
|
' dx test e2e quantify apps/quantify/e2e/health/health.e2e-spec.ts # 运行 Quantify E2E 文件',
|
|
98
99
|
' dx test unit backend apps/backend/src/modules/chat/chat.service.spec.ts # 运行单个后端单测文件',
|
|
99
100
|
' dx test e2e all # 不受支持,必须指定 target 和 path',
|
|
@@ -254,13 +255,13 @@ start 命令用法:
|
|
|
254
255
|
case 'test':
|
|
255
256
|
console.log(`
|
|
256
257
|
test 命令用法:
|
|
257
|
-
dx test [type] [target] [path] [-t pattern]
|
|
258
|
+
dx test [type] [target] [path] [-t pattern|--name pattern]
|
|
258
259
|
|
|
259
260
|
参数说明:
|
|
260
261
|
type: e2e, unit (默认: e2e)
|
|
261
262
|
target: 由 commands.json 的 test.<type>.<target> 决定
|
|
262
263
|
path: guarded e2e target 必须提供文件或目录路径
|
|
263
|
-
-t pattern: 指定测试用例名称模式,需要和 path 一起使用
|
|
264
|
+
-t pattern / --name pattern: 指定测试用例名称模式,需要和 path 一起使用
|
|
264
265
|
|
|
265
266
|
限制说明:
|
|
266
267
|
guarded E2E target 禁止无路径全量执行。
|
package/lib/codex-initial.js
CHANGED
|
@@ -59,6 +59,8 @@ async function copyDirMerge({ srcDir, dstDir }) {
|
|
|
59
59
|
|
|
60
60
|
for (const file of files) {
|
|
61
61
|
const rel = relative(srcDir, file)
|
|
62
|
+
const topLevelDir = rel.split('/')[0]
|
|
63
|
+
if (DEPRECATED_SKILL_DIRS.includes(topLevelDir)) continue
|
|
62
64
|
const target = join(dstDir, rel)
|
|
63
65
|
await ensureDir(dirname(target))
|
|
64
66
|
await fs.copyFile(file, target)
|
|
@@ -91,6 +93,7 @@ export async function runCodexInitial(options = {}) {
|
|
|
91
93
|
await ensureDir(target.dir)
|
|
92
94
|
await removeDeprecatedSkillDirs(target.dir)
|
|
93
95
|
const copyStats = await copyDirMerge({ srcDir: srcSkillsDir, dstDir: target.dir })
|
|
96
|
+
await removeDeprecatedSkillDirs(target.dir)
|
|
94
97
|
stats.push({ ...target, ...copyStats })
|
|
95
98
|
}
|
|
96
99
|
|
package/package.json
CHANGED
|
@@ -9,23 +9,76 @@ description: Use when backend、NestJS、领域异常治理或错误处理审查
|
|
|
9
9
|
|
|
10
10
|
先判断项目是否已经具备统一错误处理基础设施,再扫描业务代码是否绕过 `DomainException` / `ErrorCode` 体系。默认只输出审计结果和修复建议,不自动改代码;只有用户明确要求时才进入自动修复。
|
|
11
11
|
|
|
12
|
+
## 扫描范围
|
|
13
|
+
|
|
14
|
+
脚本不预设任何项目路径,需要通过 `--src-dir` / `--e2e-dir` 显式传入。执行前必须先完成项目探索(见下方步骤 0)。
|
|
15
|
+
|
|
16
|
+
以下路径/后缀始终排除,不会产生误报:
|
|
17
|
+
|
|
18
|
+
| 排除类别 | 路径/后缀 |
|
|
19
|
+
|----------|-----------|
|
|
20
|
+
| 单元测试 | `*.spec.ts`、`*.test.ts`、`*.e2e-spec.ts` |
|
|
21
|
+
| 测试辅助 | `*.mock.ts`、`*.stub.ts`、`*.fixture.ts`、`fixtures/`、`test-utils/`、`testing/`、`__tests__/`、`__test__/` |
|
|
22
|
+
| 基础设施 | `*/filters/`、`prisma/`、`scripts/` |
|
|
23
|
+
| 异常定义 | `*.exception.ts` |
|
|
24
|
+
| 入口文件 | `main.ts` |
|
|
25
|
+
|
|
26
|
+
只有在用户**明确要求**评估测试债务时,才传 `--e2e-dir` + `--scope e2e` 扫描测试代码。
|
|
27
|
+
|
|
12
28
|
## 快速开始
|
|
13
29
|
|
|
14
|
-
|
|
30
|
+
### 步骤 0:项目探索(每个项目首次执行时必须完成)
|
|
31
|
+
|
|
32
|
+
在调用脚本前,先探索项目结构,识别出后端生产代码和测试代码的实际路径。方法:
|
|
33
|
+
|
|
34
|
+
1. 查看项目根目录结构(`ls` 或 Glob)
|
|
35
|
+
2. 识别后端应用目录(可能是 `apps/backend/src`、`src`、`server/src` 等)
|
|
36
|
+
3. 识别测试目录(可能是 `apps/backend/e2e`、`test`、`e2e`、`__tests__` 等)
|
|
37
|
+
4. 如果是 monorepo,可能有多个后端服务,每个都需要单独传 `--src-dir`
|
|
38
|
+
|
|
39
|
+
典型示例:
|
|
40
|
+
|
|
41
|
+
| 项目类型 | 生产代码 | 测试代码 |
|
|
42
|
+
|----------|----------|----------|
|
|
43
|
+
| NestJS monorepo | `apps/backend/src` | `apps/backend/e2e` |
|
|
44
|
+
| 单体 NestJS | `src` | `test` 或 `e2e` |
|
|
45
|
+
| 多服务 monorepo | `apps/api/src`、`apps/worker/src` | `apps/api/e2e`、`apps/worker/e2e` |
|
|
46
|
+
|
|
47
|
+
### 步骤 1:审计生产代码
|
|
48
|
+
|
|
49
|
+
将探索到的路径传给脚本(`--src-dir` 可传多个):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
SKILL_HOME="${SKILL_HOME:-$HOME/.claude/skills}"
|
|
53
|
+
python "$SKILL_HOME/error-handling-audit-fixer/scripts/error_handling_audit.py" \
|
|
54
|
+
--workspace "$PWD" \
|
|
55
|
+
--src-dir <探索到的生产代码路径>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
示例(当前项目):
|
|
15
59
|
|
|
16
60
|
```bash
|
|
17
|
-
|
|
18
|
-
python "$CODEX_HOME/skills/error-handling-audit-fixer/scripts/error_handling_audit.py" \
|
|
61
|
+
python "$SKILL_HOME/error-handling-audit-fixer/scripts/error_handling_audit.py" \
|
|
19
62
|
--workspace "$PWD" \
|
|
20
|
-
--
|
|
63
|
+
--src-dir apps/backend/src
|
|
21
64
|
```
|
|
22
65
|
|
|
23
|
-
|
|
66
|
+
多服务示例:
|
|
24
67
|
|
|
25
68
|
```bash
|
|
26
|
-
python "$
|
|
69
|
+
python "$SKILL_HOME/error-handling-audit-fixer/scripts/error_handling_audit.py" \
|
|
27
70
|
--workspace "$PWD" \
|
|
28
|
-
--
|
|
71
|
+
--src-dir apps/api/src \
|
|
72
|
+
--src-dir apps/worker/src
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 步骤 2:(仅在用户明确要求时)审计测试代码
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python "$SKILL_HOME/error-handling-audit-fixer/scripts/error_handling_audit.py" \
|
|
79
|
+
--workspace "$PWD" \
|
|
80
|
+
--e2e-dir <探索到的测试代码路径> \
|
|
81
|
+
--scope e2e \
|
|
29
82
|
--output-json /tmp/error-handling-audit.json
|
|
30
83
|
```
|
|
31
84
|
|
|
@@ -37,7 +90,7 @@ python "$CODEX_HOME/skills/error-handling-audit-fixer/scripts/error_handling_aud
|
|
|
37
90
|
|
|
38
91
|
## 执行流程
|
|
39
92
|
|
|
40
|
-
1.
|
|
93
|
+
1. 默认只扫描 `apps/backend/src`(生产代码)。测试代码、E2E、prisma seed、脚本等非生产路径全部排除。只有在用户明确要求评估测试债务时,才传 `--scope e2e` 扫描测试代码。
|
|
41
94
|
2. 先判断基础设施状态:
|
|
42
95
|
- 是否存在 `DomainException`
|
|
43
96
|
- 是否存在 `ErrorCode`
|
|
@@ -56,47 +109,31 @@ python "$CODEX_HOME/skills/error-handling-audit-fixer/scripts/error_handling_aud
|
|
|
56
109
|
6. 只有在用户明确说“自动修复”“直接改”或等价表述时,才进入落代码阶段。
|
|
57
110
|
7. 若脚本结果与实际代码不一致,必须抽样打开命中文件复核,不要把脚本结果当成绝对真相。
|
|
58
111
|
|
|
59
|
-
##
|
|
112
|
+
## 审计命令(rg 快速复核)
|
|
60
113
|
|
|
61
|
-
|
|
114
|
+
以下 rg 命令用于手工复核,将 `<SRC_DIR>` 替换为步骤 0 探索到的实际路径:
|
|
62
115
|
|
|
63
116
|
```bash
|
|
64
117
|
rg "new (BadRequestException|UnauthorizedException|ForbiddenException|NotFoundException|HttpException|InternalServerErrorException)\(" \
|
|
65
|
-
|
|
66
|
-
--glob '!*spec.ts' \
|
|
118
|
+
<SRC_DIR> \
|
|
119
|
+
--glob '!*spec.ts' --glob '!*test.ts' \
|
|
67
120
|
--glob '!*exception.ts' \
|
|
68
|
-
--glob '
|
|
69
|
-
--glob '
|
|
121
|
+
--glob '!*/filters/**' \
|
|
122
|
+
--glob '!*main.ts'
|
|
70
123
|
```
|
|
71
124
|
|
|
72
125
|
```bash
|
|
73
|
-
rg "new Error\("
|
|
126
|
+
rg "new Error\(" <SRC_DIR> --glob '!*spec.ts' --glob '!*test.ts'
|
|
74
127
|
```
|
|
75
128
|
|
|
76
129
|
```bash
|
|
77
|
-
rg "new DomainException\([^)]*$" -A3
|
|
130
|
+
rg "new DomainException\([^)]*$" -A3 <SRC_DIR>
|
|
78
131
|
```
|
|
79
132
|
|
|
80
133
|
```bash
|
|
81
|
-
rg "DomainException\([^)]*[\u4e00-\u9fa5]"
|
|
134
|
+
rg "DomainException\([^)]*[\u4e00-\u9fa5]" <SRC_DIR> \
|
|
82
135
|
--glob '!*spec.ts' \
|
|
83
|
-
--glob '
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
如果只想快速看生产代码,可优先改成:
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
python "$CODEX_HOME/skills/error-handling-audit-fixer/scripts/error_handling_audit.py" \
|
|
90
|
-
--workspace "$PWD" \
|
|
91
|
-
--scope src
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
如果只想看 E2E 存量问题,可改成:
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
python "$CODEX_HOME/skills/error-handling-audit-fixer/scripts/error_handling_audit.py" \
|
|
98
|
-
--workspace "$PWD" \
|
|
99
|
-
--scope e2e
|
|
136
|
+
--glob '!*/common/exceptions/**'
|
|
100
137
|
```
|
|
101
138
|
|
|
102
139
|
## 修复准则
|
|
@@ -60,16 +60,34 @@ def parse_args() -> argparse.Namespace:
|
|
|
60
60
|
parser = argparse.ArgumentParser(description="审计后端错误处理是否绕过 DomainException / ErrorCode 体系")
|
|
61
61
|
parser.add_argument("--workspace", required=True, help="仓库根目录")
|
|
62
62
|
parser.add_argument(
|
|
63
|
-
"--
|
|
63
|
+
"--src-dir",
|
|
64
|
+
action="append",
|
|
65
|
+
default=None,
|
|
66
|
+
help="生产代码目录(相对于 workspace,可重复),如 apps/backend/src",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--e2e-dir",
|
|
64
70
|
action="append",
|
|
65
71
|
default=None,
|
|
66
|
-
help="
|
|
72
|
+
help="测试代码目录(相对于 workspace,可重复),如 apps/backend/e2e",
|
|
67
73
|
)
|
|
68
74
|
parser.add_argument(
|
|
69
75
|
"--scope",
|
|
70
76
|
choices=["all", "src", "e2e"],
|
|
71
|
-
default="
|
|
72
|
-
help="
|
|
77
|
+
default="src",
|
|
78
|
+
help="扫描范围:src=仅生产代码(默认),e2e=仅测试代码,all=src+e2e",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--include-glob",
|
|
82
|
+
action="append",
|
|
83
|
+
default=None,
|
|
84
|
+
help="附加扫描 glob(覆盖自动推导),可重复传入",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--exclude-pattern",
|
|
88
|
+
action="append",
|
|
89
|
+
default=None,
|
|
90
|
+
help="额外排除路径片段(可重复),如 prisma/ scripts/",
|
|
73
91
|
)
|
|
74
92
|
parser.add_argument("--output-json", help="输出 JSON 文件路径")
|
|
75
93
|
parser.add_argument(
|
|
@@ -95,18 +113,44 @@ def iter_files(workspace: Path, globs: Iterable[str]) -> list[Path]:
|
|
|
95
113
|
return sorted(files)
|
|
96
114
|
|
|
97
115
|
|
|
98
|
-
|
|
116
|
+
SKIP_SUFFIXES = (
|
|
117
|
+
".spec.ts",
|
|
118
|
+
".test.ts",
|
|
119
|
+
".e2e-spec.ts",
|
|
120
|
+
".exception.ts",
|
|
121
|
+
".mock.ts",
|
|
122
|
+
".stub.ts",
|
|
123
|
+
".fixture.ts",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
SKIP_DIR_FRAGMENTS = (
|
|
127
|
+
"__tests__/",
|
|
128
|
+
"__test__/",
|
|
129
|
+
"test-utils/",
|
|
130
|
+
"testing/",
|
|
131
|
+
"fixtures/",
|
|
132
|
+
"/filters/",
|
|
133
|
+
"prisma/",
|
|
134
|
+
"scripts/",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
SKIP_FILENAMES = ("main.ts",)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def should_skip(path: Path, extra_excludes: list[str] | None = None) -> bool:
|
|
99
141
|
path_text = path.as_posix()
|
|
100
|
-
if
|
|
101
|
-
return True
|
|
102
|
-
if path_text.endswith(".exception.ts"):
|
|
103
|
-
return True
|
|
104
|
-
if path_text == "apps/backend/src/main.ts":
|
|
105
|
-
return True
|
|
106
|
-
if path_text.startswith("apps/backend/src/common/filters/"):
|
|
107
|
-
return True
|
|
108
|
-
if path_text.startswith("apps/backend/src/common/exceptions/"):
|
|
142
|
+
if path.name in SKIP_FILENAMES:
|
|
109
143
|
return True
|
|
144
|
+
for suffix in SKIP_SUFFIXES:
|
|
145
|
+
if path_text.endswith(suffix):
|
|
146
|
+
return True
|
|
147
|
+
for fragment in SKIP_DIR_FRAGMENTS:
|
|
148
|
+
if fragment in path_text:
|
|
149
|
+
return True
|
|
150
|
+
if extra_excludes:
|
|
151
|
+
for pattern in extra_excludes:
|
|
152
|
+
if pattern in path_text:
|
|
153
|
+
return True
|
|
110
154
|
return False
|
|
111
155
|
|
|
112
156
|
|
|
@@ -114,12 +158,14 @@ def line_no(content: str, index: int) -> int:
|
|
|
114
158
|
return content.count("\n", 0, index) + 1
|
|
115
159
|
|
|
116
160
|
|
|
117
|
-
def
|
|
161
|
+
def build_globs(scope: str, src_dirs: list[str], e2e_dirs: list[str]) -> list[str]:
|
|
118
162
|
if scope == "src":
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
163
|
+
dirs = src_dirs
|
|
164
|
+
elif scope == "e2e":
|
|
165
|
+
dirs = e2e_dirs
|
|
166
|
+
else:
|
|
167
|
+
dirs = src_dirs + e2e_dirs
|
|
168
|
+
return [f"{d}/**/*.ts" for d in dirs]
|
|
123
169
|
|
|
124
170
|
|
|
125
171
|
def safe_read_text(path: Path) -> str:
|
|
@@ -284,30 +330,32 @@ def find_constructor_calls(content: str, constructor_name: str) -> list[tuple[in
|
|
|
284
330
|
return matches
|
|
285
331
|
|
|
286
332
|
|
|
287
|
-
def collect_foundation_status(workspace: Path) -> FoundationStatus:
|
|
288
|
-
backend_root = workspace / "apps/backend/src"
|
|
289
|
-
all_ts_files = sorted(backend_root.glob("**/*.ts"))
|
|
333
|
+
def collect_foundation_status(workspace: Path, src_dirs: list[str]) -> FoundationStatus:
|
|
290
334
|
has_domain_exception = False
|
|
291
335
|
has_error_code = False
|
|
292
336
|
has_exception_filters = False
|
|
293
337
|
has_module_exceptions_dir = False
|
|
294
338
|
has_structured_request_id_signal = False
|
|
295
339
|
|
|
296
|
-
for
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if not content:
|
|
340
|
+
for src_dir in src_dirs:
|
|
341
|
+
root = workspace / src_dir
|
|
342
|
+
if not root.is_dir():
|
|
300
343
|
continue
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
344
|
+
for path in sorted(root.glob("**/*.ts")):
|
|
345
|
+
path_text = path.as_posix()
|
|
346
|
+
content = safe_read_text(path)
|
|
347
|
+
if not content:
|
|
348
|
+
continue
|
|
349
|
+
if re.search(r"\bclass\s+DomainException\b", content):
|
|
350
|
+
has_domain_exception = True
|
|
351
|
+
if re.search(r"\b(enum|const)\s+ErrorCode\b", content) or re.search(r"\bErrorCode\.[A-Z0-9_]+\b", content):
|
|
352
|
+
has_error_code = True
|
|
353
|
+
if "/filters/" in path_text and re.search(r"ExceptionFilter|Catch\s*\(", content):
|
|
354
|
+
has_exception_filters = True
|
|
355
|
+
if "/exceptions/" in path_text and "common/exceptions" not in path_text:
|
|
356
|
+
has_module_exceptions_dir = True
|
|
357
|
+
if "requestId" in content and re.search(r"\b(args|code)\b", content):
|
|
358
|
+
has_structured_request_id_signal = True
|
|
311
359
|
|
|
312
360
|
return FoundationStatus(
|
|
313
361
|
has_domain_exception=has_domain_exception,
|
|
@@ -506,9 +554,19 @@ def print_report(foundations: FoundationStatus, findings: list[Finding]) -> None
|
|
|
506
554
|
def main() -> int:
|
|
507
555
|
args = parse_args()
|
|
508
556
|
workspace = Path(args.workspace).resolve()
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
557
|
+
|
|
558
|
+
src_dirs: list[str] = args.src_dir or []
|
|
559
|
+
e2e_dirs: list[str] = args.e2e_dir or []
|
|
560
|
+
extra_excludes: list[str] | None = args.exclude_pattern
|
|
561
|
+
|
|
562
|
+
if not src_dirs and not e2e_dirs and not args.include_glob:
|
|
563
|
+
print("错误:必须通过 --src-dir 或 --e2e-dir 指定扫描目录,或通过 --include-glob 指定 glob 模式")
|
|
564
|
+
print("示例:--src-dir apps/backend/src --e2e-dir apps/backend/e2e")
|
|
565
|
+
return 1
|
|
566
|
+
|
|
567
|
+
globs = args.include_glob or build_globs(args.scope, src_dirs, e2e_dirs)
|
|
568
|
+
files = [path for path in iter_files(workspace, globs) if not should_skip(path.relative_to(workspace), extra_excludes)]
|
|
569
|
+
foundations = collect_foundation_status(workspace, src_dirs or e2e_dirs)
|
|
512
570
|
|
|
513
571
|
findings: list[Finding] = []
|
|
514
572
|
for path in files:
|
|
@@ -442,7 +442,49 @@ MSG
|
|
|
442
442
|
|
|
443
443
|
若继续:`ROUND += 1`,回到 4.1。
|
|
444
444
|
|
|
445
|
-
若结束且 `CODE_CHANGED_SINCE_LAST_CHECK=true`:执行最终验证——按串行顺序跑 `dx lint` -> `dx build affected --dev` -> 关联测试,有错即停。有失败则修复并 commit/push
|
|
445
|
+
若结束且 `CODE_CHANGED_SINCE_LAST_CHECK=true`:执行最终验证——按串行顺序跑 `dx lint` -> `dx build affected --dev` -> 关联测试,有错即停。有失败则修复并 commit/push,无失败则进入 4.7 发布验证总结。
|
|
446
|
+
|
|
447
|
+
若结束且 `CODE_CHANGED_SINCE_LAST_CHECK=false`(本轮零问题直接通过):同样进入 4.7,基于最近一次验证结果发布总结。
|
|
448
|
+
|
|
449
|
+
### 4.7 发布验证总结报告到 PR
|
|
450
|
+
|
|
451
|
+
最终验证通过后,在 PR 上发布一条验证总结,让审查者一眼看清本次交付的质量门禁结果。
|
|
452
|
+
|
|
453
|
+
收集并汇总以下信息:
|
|
454
|
+
- **审查轮数**:共经历了几轮审查修复循环
|
|
455
|
+
- **Lint 结果**:通过/失败(最终状态)
|
|
456
|
+
- **构建结果**:通过/失败(最终状态)
|
|
457
|
+
- **测试执行明细**:列出实际执行的每条测试命令及其结果(如 `dx test e2e backend apps/backend/e2e/ai-model/virtual-model.e2e-spec.ts` -> 通过),未执行测试的类别注明"无相关改动,跳过"
|
|
458
|
+
- **代码审查**:共发现多少问题、修复多少、拒绝多少
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
gh pr comment <PR_NUMBER> --body-file - <<'MSG'
|
|
462
|
+
## ✅ 验证总结
|
|
463
|
+
|
|
464
|
+
### 门禁结果
|
|
465
|
+
|
|
466
|
+
| 步骤 | 状态 | 备注 |
|
|
467
|
+
|------|------|------|
|
|
468
|
+
| Lint (`dx lint`) | ✅ 通过 | |
|
|
469
|
+
| 构建 (`dx build affected --dev`) | ✅ 通过 | |
|
|
470
|
+
| 后端 E2E | ✅ 通过 | `dx test e2e backend <实际执行的文件/目录>` |
|
|
471
|
+
| 前端单测 | ⏭️ 跳过 | 无相关改动 |
|
|
472
|
+
| 管理端单测 | ⏭️ 跳过 | 无相关改动 |
|
|
473
|
+
|
|
474
|
+
### 审查统计
|
|
475
|
+
|
|
476
|
+
- 审查轮数:N
|
|
477
|
+
- 发现问题:X 个(Critical: a / Major: b / Minor: c)
|
|
478
|
+
- 已修复:Y 个
|
|
479
|
+
- 拒绝修复:Z 个(附理由见上方修复报告)
|
|
480
|
+
|
|
481
|
+
### 结论
|
|
482
|
+
|
|
483
|
+
所有质量门禁通过,PR 可合并。
|
|
484
|
+
MSG
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
> **注意**:表格中的"状态"和"备注"列必须基于实际执行结果填写,不能编造。若某步骤失败后经修复再次通过,标注"✅ 通过(修复后重跑)"。测试命令列必须写出实际执行的完整命令,不能用占位符。
|
|
446
488
|
|
|
447
489
|
---
|
|
448
490
|
|