fix(quality): close type and lint ratchets

This commit is contained in:
jxxghp
2026-08-26 21:55:01 +08:00
parent 1f7fac2b28
commit 6062b06610
10 changed files with 2354 additions and 357 deletions
+2 -6
View File
@@ -963,13 +963,9 @@ class MessageQueueManager(metaclass=SingletonClass):
context = copy_context() context = copy_context()
call = partial(self._send, *args, **kwargs) call = partial(self._send, *args, **kwargs)
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
submission = partial(loop.run_in_executor, None, context.run, call)
# 默认执行器保持空底层上下文,渠道调用只使用当前消息快照。 # 默认执行器保持空底层上下文,渠道调用只使用当前消息快照。
await Context().run( await Context().run(submission)
loop.run_in_executor,
None,
context.run,
call,
)
return return
self.queue.put({ self.queue.put({
"args": args, "args": args,
+20 -3
View File
@@ -83,8 +83,8 @@
| 批次 | 叶子目标 | 状态 | 当前证据/停止条件 | | 批次 | 叶子目标 | 状态 | 当前证据/停止条件 |
|---|---|---|---| |---|---|---|---|
| 0 | 历史任务清账、现行架构图、外部契约核对和宿主基线对齐 | 已本地验证 | #6472 遗漏的 dependency fixture 和 Schema 导出清单已对齐;架构契约 `71 passed` | | 0 | 历史任务清账、现行架构图、外部契约核对和宿主基线对齐 | 已推送 | `d234c7132`;远端同 SHAahead/behind `0/0`;架构契约 `71 passed` |
| 1 | Mypy fail-closed,并把 Ruff/Mypy 已下降债务固化为真实低水位 | 待批次 0 | Mypy 1.18.2 在 `message.py` 内部错误退出 2,但脚本误报通过;Ruff 978 对旧基线 1623 | | 1 | Mypy fail-closed,并把 Ruff/Mypy 已下降债务固化为真实低水位 | 已本地验证 | Mypy 完整低水位 11994、Ruff 976;专项 `33 passed`;架构契约 `71 passed`Pylint `10.00/10` |
| 2 | 用全量串行测试初始化非零 Coverage 低水位,并补齐 CI/文档防回退契约 | 待批次 1 | fixture 当前 Application/Domain 均为 0%;最近 CI 仅作参考,必须在最终代码快照本地重建 | | 2 | 用全量串行测试初始化非零 Coverage 低水位,并补齐 CI/文档防回退契约 | 待批次 1 | fixture 当前 Application/Domain 均为 0%;最近 CI 仅作参考,必须在最终代码快照本地重建 |
| 3 | 收口阶段 62 遗留的 QQ Gateway heartbeat Timer 所有权 | 待批次 2 | Timer 只 cancel 不 joinGateway 主线程可能在 heartbeat 仍执行时报告停止成功 | | 3 | 收口阶段 62 遗留的 QQ Gateway heartbeat Timer 所有权 | 待批次 2 | Timer 只 cancel 不 joinGateway 主线程可能在 heartbeat 仍执行时报告停止成功 |
| Final | 全仓回归、插件兼容复核、台账定稿和远端一致性验证 | 待前置批次 | 所有准入项已推送;全量测试和适用门禁通过;本地/远端 0/0 | | Final | 全仓回归、插件兼容复核、台账定稿和远端一致性验证 | 待前置批次 | 所有准入项已推送;全量测试和适用门禁通过;本地/远端 0/0 |
@@ -106,7 +106,8 @@
* `baseline.py --check-host``scripts/schema/exports.py --check` 和相关架构契约测试通过, * `baseline.py --check-host``scripts/schema/exports.py --check` 和相关架构契约测试通过,
测试结果为 `71 passed` 测试结果为 `71 passed`
待完成:提交并推送本批次,记录远端证据。 交付证据:提交 `d234c7132` 已推送到 `origin/v3``git ls-remote` 返回同一 SHA
`HEAD...origin/v3``0/0`,且本地 HEAD 是远端祖先。
### 批次 1Ruff/Mypy 低水位闭环 ### 批次 1Ruff/Mypy 低水位闭环
@@ -121,6 +122,20 @@
完整扫描正常退出并生成真实 Mypy fixtureRuff/Mypy fixture 与当前结果完全相等; 完整扫描正常退出并生成真实 Mypy fixtureRuff/Mypy fixture 与当前结果完全相等;
针对性测试、两条门禁和适用静态检查通过;批次独立提交推送。 针对性测试、两条门禁和适用静态检查通过;批次独立提交推送。
本地验收结果:
* 消息 executor 提交改为零参数 `partial`,保持空底层上下文提交和 worker 请求快照,
Mypy 1.18.2 不再 INTERNAL ERROR;同步 fake loop 已验证提交阶段无关联 ID、渠道回调可见 ID;
* Mypy runner 固定使用 CI 的 Linux/Python 3.14 分析目标,只接受退出码 0/1,拒绝 stderr、
缺失/重复摘要和摘要计数不一致;完整扫描覆盖 601 个文件、11994 项。旧 fixture 的
342 个文件、6209 项已确认是截断快照;macOS 与 Linux 目标生成结果逐字一致;
* Ruff/Mypy 将增长与低水位滞后分开;已有基线存在增长时 `--write` 被拒绝,
只有纯下降或缺失的新基线可以写入;
* Ruff fixture 从 1623 收紧到 976Mypy 和 Ruff 默认路径复跑均通过;
* 质量/CI/上下文/严格类型专项 `33 passed`,架构契约 `71 passed`,改动文件 Pylint `10.00/10`
待完成:提交推送并记录远端证据。
### 批次 2Coverage 低水位闭环 ### 批次 2Coverage 低水位闭环
已确认的既有缺口:Coverage 脚本和 CI 已落地,但 fixture 的 Application/Domain 都是 已确认的既有缺口:Coverage 脚本和 CI 已落地,但 fixture 的 Application/Domain 都是
@@ -178,6 +193,8 @@ git rev-list --left-right --count HEAD...origin/v3
| 2026-08-26 | 复验宿主基线 | 只剩 #6472 引入的两条合法依赖尚未写入 fixture | | 2026-08-26 | 复验宿主基线 | 只剩 #6472 引入的两条合法依赖尚未写入 fixture |
| 2026-08-26 | 收口 #6472 生成契约 | dependency fixture 与 Schema 导出清单已更新;架构契约 `71 passed` | | 2026-08-26 | 收口 #6472 生成契约 | dependency fixture 与 Schema 导出清单已更新;架构契约 `71 passed` |
| 2026-08-26 | 后台 owner 历史余项审计 | 仅 QQ heartbeat Timer 满足阶段 62 既有合同和两天准入条件,其余候选关闭或排除 | | 2026-08-26 | 后台 owner 历史余项审计 | 仅 QQ heartbeat Timer 满足阶段 62 既有合同和两天准入条件,其余候选关闭或排除 |
| 2026-08-26 | 批次 0 交付 | `d234c7132` 已推送;远端同 SHAahead/behind `0/0` |
| 2026-08-26 | 批次 1 本地验收 | Mypy 11994、Ruff 976;专项 `33 passed`;架构契约 `71 passed`Pylint `10.00/10` |
## 七、本轮停止条件 ## 七、本轮停止条件
+89 -19
View File
@@ -1,9 +1,8 @@
"""为宿主源码维护 mypy 类型错误只降不增的基线。 """为宿主源码维护 mypy 类型错误只降不增的基线。
与 ``complexity.py`` 同一模式:AST/subprocess 收集当前计数,与 JSON 基线对比, 与 ``complexity.py`` 同一模式:AST/subprocess 收集当前计数,与 JSON 基线对比,
删除和减少均合法,新增文件或错误码、既有计数增长都会被拒绝。 基线按 文件 -> 错误码 -> 数量 三级组织。任何增长都会被拒绝;债务下降后也必须用
基线按 文件 -> 错误码 -> 数量 三级组织,修复单个文件后可用 ``--write`` 收紧基线, ``--write`` 固化新的低水位,避免已经修复的错误重新获得回退额度。
不影响其他文件的存量豁免。
""" """
from __future__ import annotations from __future__ import annotations
@@ -22,19 +21,23 @@ DEFAULT_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/mypy-baseline.jso
# 只扫描宿主源码;app/plugins 是运行时插件副本,质量由插件市场链路自行管理。 # 只扫描宿主源码;app/plugins 是运行时插件副本,质量由插件市场链路自行管理。
MYPY_TARGETS = ("app",) MYPY_TARGETS = ("app",)
MYPY_EXCLUDES = ("app/plugins",) MYPY_EXCLUDES = ("app/plugins",)
MYPY_PLATFORM = "linux"
# 形如 app/foo.py:12: error: 消息说明 [error-code];个别错误可能缺代码。 # 形如 app/foo.py:12: error: 消息说明 [error-code];个别错误可能缺代码。
_ERROR_LINE = re.compile(r"^(?P<path>.+?):\d+(?::\d+)?: error: .+?(?:\s+\[(?P<code>[a-z0-9-]+)\])?$") _ERROR_LINE = re.compile(r"^(?P<path>.+?):\d+(?::\d+)?: error: .+?(?:\s+\[(?P<code>[a-z0-9-]+)\])?$")
_ERROR_SUMMARY = re.compile(r"^Found (?P<count>\d+) errors? in ")
def run_mypy() -> str: def run_mypy() -> str:
"""以当前解释器运行全量 mypy 并返回 stdout(非零退出码属于预期结果""" """运行全量 mypy;只接受可完整解析的正常成功或诊断结果。"""
command = [ command = [
sys.executable, sys.executable,
"-m", "-m",
"mypy", "mypy",
"--no-incremental", "--no-incremental",
"--no-pretty", "--no-pretty",
"--platform",
MYPY_PLATFORM,
*MYPY_TARGETS, *MYPY_TARGETS,
] ]
for pattern in MYPY_EXCLUDES: for pattern in MYPY_EXCLUDES:
@@ -46,6 +49,36 @@ def run_mypy() -> str:
text=True, text=True,
check=False, check=False,
) )
if result.returncode not in {0, 1}:
details = result.stderr.strip() or result.stdout.strip() or "mypy 执行失败"
raise RuntimeError(f"mypy 异常退出({result.returncode}):{details}")
if result.stderr.strip():
raise RuntimeError(f"mypy 在 stderr 输出异常:{result.stderr.strip()}")
parsed_total = sum(
count
for codes in parse_errors(result.stdout).values()
for count in codes.values()
)
summaries = [
match
for line in result.stdout.splitlines()
if (match := _ERROR_SUMMARY.match(line.strip()))
]
if result.returncode == 0:
if parsed_total or not any(
line.startswith("Success: no issues found")
for line in result.stdout.splitlines()
):
raise RuntimeError("mypy 成功输出缺少可验证摘要")
return result.stdout
if len(summaries) != 1:
raise RuntimeError("mypy 错误输出缺少唯一的完整摘要")
summary_total = int(summaries[0].group("count"))
if summary_total != parsed_total:
raise RuntimeError(
f"mypy 摘要与已解析错误数不一致:摘要 {summary_total},解析 {parsed_total}"
)
return result.stdout return result.stdout
@@ -66,19 +99,38 @@ def parse_errors(output: str) -> dict[str, dict[str, int]]:
return {path: dict(codes) for path, codes in sorted(report.items())} return {path: dict(codes) for path, codes in sorted(report.items())}
def classify_counts(
baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]]
) -> tuple[list[str], list[str]]:
"""把计数差异分为不可写入的回退和可固化的低水位下降。"""
regressions: list[str] = []
stale: list[str] = []
for path in sorted(baseline.keys() | current.keys()):
previous = baseline.get(path, {})
latest = current.get(path, {})
for code in sorted(previous.keys() | latest.keys()):
old_count = previous.get(code, 0)
new_count = latest.get(code, 0)
if new_count > old_count:
if old_count == 0:
regressions.append(f"{path}: 新增类型错误 [{code}] x{new_count}")
else:
regressions.append(
f"{path}: 既有错误增长 [{code}] {old_count}->{new_count}"
)
elif new_count < old_count:
stale.append(
f"{path}: 类型错误低水位未固化 [{code}] {old_count}->{new_count}"
)
return regressions, stale
def compare_counts( def compare_counts(
baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]] baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]]
) -> list[str]: ) -> list[str]:
"""返回新增文件/错误码或既有计数增长问题;删除与减少均合法""" """返回类型错误增长和尚未固化的新低水位"""
problems = [] regressions, stale = classify_counts(baseline, current)
for path, codes in current.items(): return [*regressions, *stale]
previous = baseline.get(path, {})
for code, count in codes.items():
if code not in previous:
problems.append(f"{path}: 新增类型错误 [{code}] x{count}")
elif count > previous[code]:
problems.append(f"{path}: 既有错误增长 [{code}] {previous[code]}->{count}")
return problems
def main() -> int: def main() -> int:
@@ -88,22 +140,40 @@ def main() -> int:
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE) parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
args = parser.parse_args() args = parser.parse_args()
current = parse_errors(run_mypy()) current = parse_errors(run_mypy())
baseline_exists = args.baseline.exists()
baseline = (
json.loads(args.baseline.read_text(encoding="utf-8"))
if baseline_exists
else {}
)
regressions, stale = classify_counts(baseline, current)
if args.write: if args.write:
if baseline_exists and regressions:
print("\n".join(regressions))
print("拒绝写入:当前结果包含类型错误回退,--write 只能固化下降后的低水位。")
return 1
args.baseline.parent.mkdir(parents=True, exist_ok=True) args.baseline.parent.mkdir(parents=True, exist_ok=True)
args.baseline.write_text( args.baseline.write_text(
json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n", json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8", encoding="utf-8",
) )
print(f"已写入 {args.baseline.relative_to(PROJECT_ROOT)}") display_path = (
args.baseline.relative_to(PROJECT_ROOT)
if args.baseline.is_relative_to(PROJECT_ROOT)
else args.baseline
)
print(f"已写入 {display_path}")
return 0 return 0
baseline = json.loads(args.baseline.read_text(encoding="utf-8")) problems = [*regressions, *stale]
problems = compare_counts(baseline, current)
if problems: if problems:
print("\n".join(problems)) print("\n".join(problems))
print("提示:修复后可用 --write 收紧基线;禁止为绕过门禁放宽基线。") if regressions:
print("先消除类型错误回退;存在增长时禁止用 --write 覆盖基线。")
else:
print("提示:当前只有债务下降,可用 --write 固化新的低水位。")
return 1 return 1
total = sum(count for codes in current.values() for count in codes.values()) total = sum(count for codes in current.values() for count in codes.values())
print(f"mypy ratchet 通过(存量 {total} 个类型错误,只降不增") print(f"mypy ratchet 通过(低水位已同步:{total} 个类型错误)")
return 0 return 0
+61 -18
View File
@@ -26,7 +26,14 @@ def run_ruff() -> list[dict[str, Any]]:
) )
if result.returncode not in {0, 1}: if result.returncode not in {0, 1}:
raise RuntimeError(result.stderr or result.stdout or "Ruff 执行失败") raise RuntimeError(result.stderr or result.stdout or "Ruff 执行失败")
return json.loads(result.stdout or "[]") diagnostics = json.loads(result.stdout or "[]")
if not isinstance(diagnostics, list):
raise RuntimeError("Ruff JSON 输出不是诊断列表")
if result.returncode == 0 and diagnostics:
raise RuntimeError("Ruff 返回成功但仍输出诊断")
if result.returncode == 1 and not diagnostics:
raise RuntimeError("Ruff 返回诊断状态但输出为空")
return diagnostics
def aggregate_diagnostics( def aggregate_diagnostics(
@@ -40,22 +47,40 @@ def aggregate_diagnostics(
return {path: dict(sorted(codes.items())) for path, codes in sorted(report.items())} return {path: dict(sorted(codes.items())) for path, codes in sorted(report.items())}
def classify_counts(
baseline: dict[str, dict[str, int]],
current: dict[str, dict[str, int]],
) -> tuple[list[str], list[str]]:
"""把计数差异分为不可写入的回退和可固化的低水位下降。"""
regressions: list[str] = []
stale: list[str] = []
for path in sorted(baseline.keys() | current.keys()):
previous = baseline.get(path, {})
latest = current.get(path, {})
for code in sorted(previous.keys() | latest.keys()):
old_count = previous.get(code, 0)
new_count = latest.get(code, 0)
if new_count > old_count:
if old_count == 0:
regressions.append(f"{path}: 新增 Ruff 诊断 [{code}] x{new_count}")
else:
regressions.append(
f"{path}: Ruff 诊断增长 [{code}] {old_count}->{new_count}"
)
elif new_count < old_count:
stale.append(
f"{path}: Ruff 低水位未固化 [{code}] {old_count}->{new_count}"
)
return regressions, stale
def compare_counts( def compare_counts(
baseline: dict[str, dict[str, int]], baseline: dict[str, dict[str, int]],
current: dict[str, dict[str, int]], current: dict[str, dict[str, int]],
) -> list[str]: ) -> list[str]:
"""返回新增规则或既有数量增长;修复和删除均合法""" """返回 Ruff 诊断增长和尚未固化的新低水位"""
problems = [] regressions, stale = classify_counts(baseline, current)
for path, codes in current.items(): return [*regressions, *stale]
previous = baseline.get(path, {})
for code, count in codes.items():
if code not in previous:
problems.append(f"{path}: 新增 Ruff 诊断 [{code}] x{count}")
elif count > previous[code]:
problems.append(
f"{path}: Ruff 诊断增长 [{code}] {previous[code]}->{count}"
)
return problems
def main() -> int: def main() -> int:
@@ -65,22 +90,40 @@ def main() -> int:
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE) parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
args = parser.parse_args() args = parser.parse_args()
current = aggregate_diagnostics(run_ruff()) current = aggregate_diagnostics(run_ruff())
baseline_exists = args.baseline.exists()
baseline = (
json.loads(args.baseline.read_text(encoding="utf-8"))
if baseline_exists
else {}
)
regressions, stale = classify_counts(baseline, current)
if args.write: if args.write:
if baseline_exists and regressions:
print("\n".join(regressions))
print("拒绝写入:当前结果包含 Ruff 回退,--write 只能固化下降后的低水位。")
return 1
args.baseline.parent.mkdir(parents=True, exist_ok=True) args.baseline.parent.mkdir(parents=True, exist_ok=True)
args.baseline.write_text( args.baseline.write_text(
json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n", json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8", encoding="utf-8",
) )
print(f"已写入 {args.baseline.relative_to(PROJECT_ROOT)}") display_path = (
args.baseline.relative_to(PROJECT_ROOT)
if args.baseline.is_relative_to(PROJECT_ROOT)
else args.baseline
)
print(f"已写入 {display_path}")
return 0 return 0
baseline = json.loads(args.baseline.read_text(encoding="utf-8")) problems = [*regressions, *stale]
problems = compare_counts(baseline, current)
if problems: if problems:
print("\n".join(problems)) print("\n".join(problems))
print("提示:修复后可用 --write 收紧基线;禁止为绕过门禁放宽基线。") if regressions:
print("先消除 Ruff 回退;存在增长时禁止用 --write 覆盖基线。")
else:
print("提示:当前只有债务下降,可用 --write 固化新的低水位。")
return 1 return 1
total = sum(count for codes in current.values() for count in codes.values()) total = sum(count for codes in current.values() for count in codes.values())
print(f"Ruff ratchet 通过(存量 {total} 个诊断,只降不增") print(f"Ruff ratchet 通过(低水位已同步:{total} 个诊断)")
return 0 return 0
File diff suppressed because it is too large Load Diff
+30 -255
View File
@@ -1,10 +1,4 @@
{ {
"app/adapters/cache/backends.py": {
"I001": 1
},
"app/adapters/cache/redis.py": {
"I001": 1
},
"app/adapters/external/cookiecloud.py": { "app/adapters/external/cookiecloud.py": {
"I001": 1 "I001": 1
}, },
@@ -27,9 +21,6 @@
"app/adapters/system/display/__init__.py": { "app/adapters/system/display/__init__.py": {
"I001": 1 "I001": 1
}, },
"app/adapters/system/fsproxy.py": {
"E402": 1
},
"app/adapters/system/host.py": { "app/adapters/system/host.py": {
"I001": 2 "I001": 2
}, },
@@ -39,9 +30,6 @@
"app/adapters/system/plugin/package.py": { "app/adapters/system/plugin/package.py": {
"I001": 1 "I001": 1
}, },
"app/adapters/system/resource.py": {
"I001": 1
},
"app/adapters/system/rust.py": { "app/adapters/system/rust.py": {
"I001": 1 "I001": 1
}, },
@@ -62,14 +50,9 @@
"I001": 1 "I001": 1
}, },
"app/agent/llm/capability.py": { "app/agent/llm/capability.py": {
"E402": 2,
"I001": 1 "I001": 1
}, },
"app/agent/llm/helper.py": {
"E402": 1
},
"app/agent/llm/provider.py": { "app/agent/llm/provider.py": {
"E402": 4,
"I001": 1 "I001": 1
}, },
"app/agent/llm/server_tools.py": { "app/agent/llm/server_tools.py": {
@@ -78,9 +61,6 @@
"app/agent/mcp.py": { "app/agent/mcp.py": {
"I001": 1 "I001": 1
}, },
"app/agent/memory/__init__.py": {
"E402": 4
},
"app/agent/middleware/memory.py": { "app/agent/middleware/memory.py": {
"I001": 1 "I001": 1
}, },
@@ -100,8 +80,8 @@
"I001": 1 "I001": 1
}, },
"app/agent/orchestrator.py": { "app/agent/orchestrator.py": {
"E402": 14, "E402": 11,
"I001": 3 "I001": 2
}, },
"app/agent/policy/__init__.py": { "app/agent/policy/__init__.py": {
"I001": 1 "I001": 1
@@ -119,7 +99,6 @@
"I001": 1 "I001": 1
}, },
"app/agent/prompt/__init__.py": { "app/agent/prompt/__init__.py": {
"E402": 6,
"I001": 1 "I001": 1
}, },
"app/agent/runtime.py": { "app/agent/runtime.py": {
@@ -131,13 +110,8 @@
"app/agent/skills/metadata.py": { "app/agent/skills/metadata.py": {
"I001": 1 "I001": 1
}, },
"app/agent/skills/registry.py": {
"E402": 4,
"I001": 1
},
"app/agent/tools/base.py": { "app/agent/tools/base.py": {
"E402": 5, "I001": 1
"I001": 2
}, },
"app/agent/tools/factory.py": { "app/agent/tools/factory.py": {
"I001": 1 "I001": 1
@@ -149,12 +123,8 @@
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/_plugin_tool_utils.py": { "app/agent/tools/impl/_plugin_tool_utils.py": {
"E402": 8,
"I001": 3 "I001": 3
}, },
"app/agent/tools/impl/_terminal_session.py": {
"E402": 1
},
"app/agent/tools/impl/_torrent_search_utils.py": { "app/agent/tools/impl/_torrent_search_utils.py": {
"I001": 1 "I001": 1
}, },
@@ -162,7 +132,6 @@
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/add_download_tasks.py": { "app/agent/tools/impl/add_download_tasks.py": {
"E402": 7,
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/add_subscribe.py": { "app/agent/tools/impl/add_subscribe.py": {
@@ -175,7 +144,6 @@
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/create_agent_task.py": { "app/agent/tools/impl/create_agent_task.py": {
"E402": 3,
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/delete_custom_filter_rule.py": { "app/agent/tools/impl/delete_custom_filter_rule.py": {
@@ -202,9 +170,6 @@
"app/agent/tools/impl/list_directory.py": { "app/agent/tools/impl/list_directory.py": {
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/query_agent_tasks.py": {
"E402": 1
},
"app/agent/tools/impl/query_builtin_filter_rules.py": { "app/agent/tools/impl/query_builtin_filter_rules.py": {
"I001": 1 "I001": 1
}, },
@@ -260,7 +225,6 @@
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/recognize_media.py": { "app/agent/tools/impl/recognize_media.py": {
"E402": 6,
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/reload_plugin.py": { "app/agent/tools/impl/reload_plugin.py": {
@@ -270,7 +234,6 @@
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/scrape_metadata.py": { "app/agent/tools/impl/scrape_metadata.py": {
"E402": 6,
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/search_media.py": { "app/agent/tools/impl/search_media.py": {
@@ -285,14 +248,10 @@
"app/agent/tools/impl/search_torrents.py": { "app/agent/tools/impl/search_torrents.py": {
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/search_web.py": {
"E402": 1
},
"app/agent/tools/impl/send_local_file.py": { "app/agent/tools/impl/send_local_file.py": {
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/send_voice_message.py": { "app/agent/tools/impl/send_voice_message.py": {
"E402": 3,
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/test_site.py": { "app/agent/tools/impl/test_site.py": {
@@ -304,9 +263,6 @@
"app/agent/tools/impl/uninstall_plugin.py": { "app/agent/tools/impl/uninstall_plugin.py": {
"I001": 1 "I001": 1
}, },
"app/agent/tools/impl/update_agent_task.py": {
"E402": 2
},
"app/agent/tools/impl/update_custom_filter_rule.py": { "app/agent/tools/impl/update_custom_filter_rule.py": {
"I001": 1 "I001": 1
}, },
@@ -398,12 +354,6 @@
"app/application/outbox.py": { "app/application/outbox.py": {
"I001": 1 "I001": 1
}, },
"app/application/plugin/catalog.py": {
"I001": 1
},
"app/application/plugin/install.py": {
"I001": 1
},
"app/application/plugin/runtime.py": { "app/application/plugin/runtime.py": {
"I001": 1 "I001": 1
}, },
@@ -470,10 +420,6 @@
"app/chain/transfer.py": { "app/chain/transfer.py": {
"E402": 21 "E402": 21
}, },
"app/cli.py": {
"E402": 4,
"I001": 1
},
"app/command.py": { "app/command.py": {
"I001": 1 "I001": 1
}, },
@@ -486,9 +432,6 @@
"app/db/adapters/outbox.py": { "app/db/adapters/outbox.py": {
"I001": 1 "I001": 1
}, },
"app/db/adapters/site.py": {
"I001": 1
},
"app/db/adapters/transaction.py": { "app/db/adapters/transaction.py": {
"I001": 1 "I001": 1
}, },
@@ -597,10 +540,6 @@
"app/doctor/__init__.py": { "app/doctor/__init__.py": {
"I001": 1 "I001": 1
}, },
"app/doctor/checks.py": {
"E402": 3,
"I001": 1
},
"app/doctor/dependencies.py": { "app/doctor/dependencies.py": {
"I001": 1 "I001": 1
}, },
@@ -608,12 +547,10 @@
"I001": 1 "I001": 1
}, },
"app/doctor/runner.py": { "app/doctor/runner.py": {
"E402": 4,
"I001": 1 "I001": 1
}, },
"app/domain/context.py": { "app/domain/context.py": {
"E731": 1, "E731": 1
"I001": 1
}, },
"app/domain/media.py": { "app/domain/media.py": {
"E731": 1 "E731": 1
@@ -660,10 +597,6 @@
"app/domain/title.py": { "app/domain/title.py": {
"I001": 1 "I001": 1
}, },
"app/factory.py": {
"E402": 9,
"I001": 2
},
"app/foundation/collections.py": { "app/foundation/collections.py": {
"I001": 1 "I001": 1
}, },
@@ -693,23 +626,18 @@
"I001": 1 "I001": 1
}, },
"app/modules/acoustid/__init__.py": { "app/modules/acoustid/__init__.py": {
"E402": 4,
"I001": 1 "I001": 1
}, },
"app/modules/anilist/__init__.py": { "app/modules/anilist/__init__.py": {
"E402": 8, "I001": 1
"I001": 2
}, },
"app/modules/anilist/anilist.py": { "app/modules/anilist/anilist.py": {
"E402": 2,
"I001": 1 "I001": 1
}, },
"app/modules/bangumi/__init__.py": { "app/modules/bangumi/__init__.py": {
"E402": 9, "I001": 1
"I001": 2
}, },
"app/modules/bangumi/bangumi.py": { "app/modules/bangumi/bangumi.py": {
"E402": 1,
"I001": 1 "I001": 1
}, },
"app/modules/discord/__init__.py": { "app/modules/discord/__init__.py": {
@@ -717,17 +645,14 @@
"I001": 1 "I001": 1
}, },
"app/modules/discord/discord.py": { "app/modules/discord/discord.py": {
"E402": 6,
"E741": 1, "E741": 1,
"F541": 2, "F541": 2,
"I001": 2 "I001": 1
}, },
"app/modules/douban/__init__.py": { "app/modules/douban/__init__.py": {
"E402": 16, "I001": 1
"I001": 2
}, },
"app/modules/douban/apiv2.py": { "app/modules/douban/apiv2.py": {
"E402": 2,
"I001": 1 "I001": 1
}, },
"app/modules/douban/scraper.py": { "app/modules/douban/scraper.py": {
@@ -738,66 +663,36 @@
"I001": 1 "I001": 1
}, },
"app/modules/emby/emby.py": { "app/modules/emby/emby.py": {
"E402": 6,
"F541": 33, "F541": 33,
"I001": 2 "I001": 1
}, },
"app/modules/fanart/__init__.py": { "app/modules/fanart/__init__.py": {
"E402": 5, "I001": 1
"I001": 2
}, },
"app/modules/feishu/__init__.py": { "app/modules/feishu/__init__.py": {
"F401": 1, "F401": 1,
"I001": 1 "I001": 1
}, },
"app/modules/feishu/feishu.py": { "app/modules/feishu/feishu.py": {
"E402": 10, "I001": 1
"I001": 2
}, },
"app/modules/filemanager/__init__.py": { "app/modules/filemanager/__init__.py": {
"I001": 1 "I001": 1
}, },
"app/modules/filemanager/module.py": { "app/modules/filemanager/module.py": {
"E402": 20, "I001": 1
"I001": 2
},
"app/modules/filemanager/storages/alipan.py": {
"E402": 7
},
"app/modules/filemanager/storages/alist.py": {
"E402": 7
},
"app/modules/filemanager/storages/local.py": {
"E402": 7
},
"app/modules/filemanager/storages/rclone.py": {
"E402": 6
},
"app/modules/filemanager/storages/smb.py": {
"E402": 5
},
"app/modules/filemanager/storages/u115.py": {
"E402": 7
}, },
"app/modules/filemanager/transhandler.py": { "app/modules/filemanager/transhandler.py": {
"E402": 21,
"F541": 2, "F541": 2,
"I001": 2 "I001": 1
}, },
"app/modules/filter/__init__.py": { "app/modules/filter/__init__.py": {
"I001": 1 "I001": 1
}, },
"app/modules/imdb/__init__.py": {
"E402": 5
},
"app/modules/imdb/api.py": {
"E402": 1
},
"app/modules/indexer/__init__.py": { "app/modules/indexer/__init__.py": {
"I001": 1 "I001": 1
}, },
"app/modules/indexer/parser/__init__.py": { "app/modules/indexer/parser/__init__.py": {
"E402": 5,
"I001": 1 "I001": 1
}, },
"app/modules/indexer/parser/bitpt.py": { "app/modules/indexer/parser/bitpt.py": {
@@ -810,9 +705,6 @@
"app/modules/indexer/parser/file_list.py": { "app/modules/indexer/parser/file_list.py": {
"I001": 1 "I001": 1
}, },
"app/modules/indexer/parser/gazelle.py": {
"I001": 1
},
"app/modules/indexer/parser/hddolby.py": { "app/modules/indexer/parser/hddolby.py": {
"I001": 1 "I001": 1
}, },
@@ -837,7 +729,6 @@
"I001": 1 "I001": 1
}, },
"app/modules/indexer/parser/rousi.py": { "app/modules/indexer/parser/rousi.py": {
"E402": 4,
"I001": 1 "I001": 1
}, },
"app/modules/indexer/parser/small_horse.py": { "app/modules/indexer/parser/small_horse.py": {
@@ -862,40 +753,31 @@
"I001": 1 "I001": 1
}, },
"app/modules/indexer/spider/__init__.py": { "app/modules/indexer/spider/__init__.py": {
"E402": 8,
"F541": 1, "F541": 1,
"I001": 2 "I001": 1
}, },
"app/modules/indexer/spider/haidan.py": { "app/modules/indexer/spider/haidan.py": {
"E402": 6, "I001": 1
"I001": 2
}, },
"app/modules/indexer/spider/hddolby.py": { "app/modules/indexer/spider/hddolby.py": {
"E402": 5, "I001": 1
"I001": 2
}, },
"app/modules/indexer/spider/mtorrent.py": { "app/modules/indexer/spider/mtorrent.py": {
"E402": 6, "I001": 1
"I001": 2
}, },
"app/modules/indexer/spider/rousi.py": { "app/modules/indexer/spider/rousi.py": {
"E402": 6,
"I001": 1 "I001": 1
}, },
"app/modules/indexer/spider/sunnypt.py": { "app/modules/indexer/spider/sunnypt.py": {
"E402": 4,
"I001": 1 "I001": 1
}, },
"app/modules/indexer/spider/tnode.py": { "app/modules/indexer/spider/tnode.py": {
"E402": 4, "I001": 1
"I001": 2
}, },
"app/modules/indexer/spider/torrentleech.py": { "app/modules/indexer/spider/torrentleech.py": {
"E402": 5, "I001": 1
"I001": 2
}, },
"app/modules/indexer/spider/yema.py": { "app/modules/indexer/spider/yema.py": {
"E402": 4,
"I001": 1 "I001": 1
}, },
"app/modules/jellyfin/__init__.py": { "app/modules/jellyfin/__init__.py": {
@@ -903,24 +785,19 @@
"I001": 1 "I001": 1
}, },
"app/modules/jellyfin/jellyfin.py": { "app/modules/jellyfin/jellyfin.py": {
"E402": 7,
"F541": 34, "F541": 34,
"I001": 2 "I001": 1
}, },
"app/modules/listenbrainz/__init__.py": { "app/modules/listenbrainz/__init__.py": {
"E402": 5,
"I001": 1 "I001": 1
}, },
"app/modules/lrclib/__init__.py": { "app/modules/lrclib/__init__.py": {
"E402": 6,
"I001": 1 "I001": 1
}, },
"app/modules/musicbrainz/__init__.py": { "app/modules/musicbrainz/__init__.py": {
"E402": 10,
"I001": 1 "I001": 1
}, },
"app/modules/musicbrainz/music_cache.py": { "app/modules/musicbrainz/music_cache.py": {
"E402": 5,
"I001": 1 "I001": 1
}, },
"app/modules/navidrome/__init__.py": { "app/modules/navidrome/__init__.py": {
@@ -953,10 +830,6 @@
"app/modules/qqbot/module.py": { "app/modules/qqbot/module.py": {
"I001": 1 "I001": 1
}, },
"app/modules/qqbot/qqbot.py": {
"E402": 9,
"I001": 2
},
"app/modules/rtorrent/__init__.py": { "app/modules/rtorrent/__init__.py": {
"F541": 2, "F541": 2,
"I001": 1 "I001": 1
@@ -970,11 +843,9 @@
"I001": 1 "I001": 1
}, },
"app/modules/slack/slack.py": { "app/modules/slack/slack.py": {
"E402": 6, "I001": 1
"I001": 2
}, },
"app/modules/subtitle/__init__.py": { "app/modules/subtitle/__init__.py": {
"E402": 7,
"I001": 1 "I001": 1
}, },
"app/modules/synologychat/__init__.py": { "app/modules/synologychat/__init__.py": {
@@ -996,46 +867,32 @@
"I001": 3 "I001": 3
}, },
"app/modules/theaudiodb/__init__.py": { "app/modules/theaudiodb/__init__.py": {
"E402": 8,
"I001": 1 "I001": 1
}, },
"app/modules/themoviedb/__init__.py": { "app/modules/themoviedb/__init__.py": {
"E402": 15, "I001": 1
"I001": 2
}, },
"app/modules/themoviedb/category.py": { "app/modules/themoviedb/category.py": {
"E402": 3,
"F541": 1, "F541": 1,
"I001": 1 "I001": 1
}, },
"app/modules/themoviedb/scraper.py": { "app/modules/themoviedb/scraper.py": {
"E402": 5,
"I001": 1 "I001": 1
}, },
"app/modules/themoviedb/tmdb_cache.py": { "app/modules/themoviedb/tmdb_cache.py": {
"E402": 4,
"I001": 1 "I001": 1
}, },
"app/modules/themoviedb/tmdbapi.py": { "app/modules/themoviedb/tmdbapi.py": {
"E402": 6, "I001": 1
"I001": 2
}, },
"app/modules/themoviedb/tmdbv3api/__init__.py": { "app/modules/themoviedb/tmdbv3api/__init__.py": {
"F401": 25 "F401": 25
}, },
"app/modules/themoviedb/tmdbv3api/objs/discover.py": {
"E402": 1
},
"app/modules/themoviedb/tmdbv3api/tmdb.py": { "app/modules/themoviedb/tmdbv3api/tmdb.py": {
"E402": 2,
"I001": 2
},
"app/modules/thetvdb/__init__.py": {
"E402": 4,
"I001": 1 "I001": 1
}, },
"app/modules/thetvdb/tvdb_v4_official.py": { "app/modules/thetvdb/__init__.py": {
"E402": 1 "I001": 1
}, },
"app/modules/transmission/__init__.py": { "app/modules/transmission/__init__.py": {
"F541": 2, "F541": 2,
@@ -1048,7 +905,6 @@
"I001": 1 "I001": 1
}, },
"app/modules/trimemedia/api.py": { "app/modules/trimemedia/api.py": {
"E402": 2,
"I001": 1 "I001": 1
}, },
"app/modules/trimemedia/module.py": { "app/modules/trimemedia/module.py": {
@@ -1077,8 +933,7 @@
"I001": 1 "I001": 1
}, },
"app/modules/webpush/__init__.py": { "app/modules/webpush/__init__.py": {
"E402": 4, "I001": 1
"I001": 2
}, },
"app/modules/wechat/__init__.py": { "app/modules/wechat/__init__.py": {
"F541": 3, "F541": 3,
@@ -1088,15 +943,10 @@
"F541": 1, "F541": 1,
"I001": 1 "I001": 1
}, },
"app/modules/wechat/wechatbot.py": {
"E402": 10,
"I001": 2
},
"app/modules/wechatclawbot/__init__.py": { "app/modules/wechatclawbot/__init__.py": {
"I001": 1 "I001": 1
}, },
"app/modules/wechatclawbot/wechatclawbot.py": { "app/modules/wechatclawbot/wechatclawbot.py": {
"E402": 6,
"I001": 1 "I001": 1
}, },
"app/modules/zspace/__init__.py": { "app/modules/zspace/__init__.py": {
@@ -1149,14 +999,10 @@
"app/runtime/event/errors.py": { "app/runtime/event/errors.py": {
"I001": 1 "I001": 1
}, },
"app/runtime/events.py": {
"I001": 1
},
"app/runtime/execution.py": { "app/runtime/execution.py": {
"I001": 1 "I001": 1
}, },
"app/runtime/extensions/host_module_adapter.py": { "app/runtime/extensions/host_module_adapter.py": {
"E402": 2,
"I001": 1 "I001": 1
}, },
"app/runtime/extensions/managed_resource_adapter.py": { "app/runtime/extensions/managed_resource_adapter.py": {
@@ -1166,12 +1012,8 @@
"I001": 1 "I001": 1
}, },
"app/runtime/extensions/module_manager.py": { "app/runtime/extensions/module_manager.py": {
"E402": 4,
"I001": 1 "I001": 1
}, },
"app/runtime/extensions/plugin/catalog.py": {
"E402": 5
},
"app/runtime/extensions/plugin/lifecycle.py": { "app/runtime/extensions/plugin/lifecycle.py": {
"I001": 1 "I001": 1
}, },
@@ -1182,8 +1024,7 @@
"I001": 1 "I001": 1
}, },
"app/runtime/extensions/plugin_manager.py": { "app/runtime/extensions/plugin_manager.py": {
"E402": 21, "I001": 1
"I001": 2
}, },
"app/runtime/extensions/service_config.py": { "app/runtime/extensions/service_config.py": {
"I001": 1 "I001": 1
@@ -1207,12 +1048,8 @@
"app/runtime/reload.py": { "app/runtime/reload.py": {
"I001": 1 "I001": 1
}, },
"app/runtime/settings.py": {
"I001": 1
},
"app/runtime/state.py": { "app/runtime/state.py": {
"E402": 3, "I001": 1
"I001": 2
}, },
"app/schemas/agent.py": { "app/schemas/agent.py": {
"I001": 1 "I001": 1
@@ -1223,9 +1060,6 @@
"app/schemas/context.py": { "app/schemas/context.py": {
"I001": 1 "I001": 1
}, },
"app/schemas/event.py": {
"I001": 1
},
"app/schemas/file.py": { "app/schemas/file.py": {
"I001": 1 "I001": 1
}, },
@@ -1271,9 +1105,6 @@
"app/sdk/config.py": { "app/sdk/config.py": {
"I001": 1 "I001": 1
}, },
"app/sdk/events.py": {
"I001": 1
},
"app/sdk/logging.py": { "app/sdk/logging.py": {
"I001": 1 "I001": 1
}, },
@@ -1292,21 +1123,6 @@
"app/sdk/utilities.py": { "app/sdk/utilities.py": {
"I001": 1 "I001": 1
}, },
"app/startup/initializers/agent.py": {
"E402": 3
},
"app/startup/initializers/database.py": {
"E402": 5
},
"app/startup/initializers/domain.py": {
"E402": 1
},
"app/startup/initializers/modules.py": {
"E402": 79
},
"app/startup/initializers/plugins.py": {
"E402": 25
},
"app/startup/lifecycle/__init__.py": { "app/startup/lifecycle/__init__.py": {
"E402": 27, "E402": 27,
"F401": 1 "F401": 1
@@ -1314,9 +1130,6 @@
"app/workflow/__init__.py": { "app/workflow/__init__.py": {
"F401": 1 "F401": 1
}, },
"scripts/architecture/baseline.py": {
"I001": 1
},
"scripts/architecture/task_ownership.py": { "scripts/architecture/task_ownership.py": {
"I001": 1 "I001": 1
}, },
@@ -1432,9 +1245,6 @@
"tests/test_agent_summarization_streaming.py": { "tests/test_agent_summarization_streaming.py": {
"I001": 1 "I001": 1
}, },
"tests/test_agent_task_run_migration.py": {
"I001": 1
},
"tests/test_agent_task_runs.py": { "tests/test_agent_task_runs.py": {
"I001": 1 "I001": 1
}, },
@@ -1465,9 +1275,6 @@
"tests/test_api_response.py": { "tests/test_api_response.py": {
"I001": 1 "I001": 1
}, },
"tests/test_architecture_ci.py": {
"I001": 1
},
"tests/test_architecture_contract_baseline.py": { "tests/test_architecture_contract_baseline.py": {
"I001": 1 "I001": 1
}, },
@@ -1605,10 +1412,6 @@
"tests/test_episode_group_recognition.py": { "tests/test_episode_group_recognition.py": {
"E402": 4 "E402": 4
}, },
"tests/test_event_contracts.py": {
"F401": 1,
"I001": 1
},
"tests/test_event_dispatch_snapshot.py": { "tests/test_event_dispatch_snapshot.py": {
"I001": 1 "I001": 1
}, },
@@ -1662,12 +1465,8 @@
"tests/test_legacy_import_compat.py": { "tests/test_legacy_import_compat.py": {
"I001": 1 "I001": 1
}, },
"tests/test_legacy_plugin_resource_imports.py": {
"I001": 1
},
"tests/test_lifecycle_shutdown.py": { "tests/test_lifecycle_shutdown.py": {
"F841": 1, "F841": 1
"I001": 1
}, },
"tests/test_llm_provider_bedrock.py": { "tests/test_llm_provider_bedrock.py": {
"I001": 1 "I001": 1
@@ -1777,9 +1576,6 @@
"tests/test_module_manager_capability_adapter.py": { "tests/test_module_manager_capability_adapter.py": {
"I001": 1 "I001": 1
}, },
"tests/test_module_method_contracts.py": {
"I001": 1
},
"tests/test_module_quality.py": { "tests/test_module_quality.py": {
"I001": 1 "I001": 1
}, },
@@ -1838,9 +1634,6 @@
"tests/test_musicbrainz_module.py": { "tests/test_musicbrainz_module.py": {
"I001": 1 "I001": 1
}, },
"tests/test_mypy_gate.py": {
"I001": 1
},
"tests/test_navidrome_module.py": { "tests/test_navidrome_module.py": {
"I001": 1 "I001": 1
}, },
@@ -1868,16 +1661,8 @@
"tests/test_plugin_dependency_service.py": { "tests/test_plugin_dependency_service.py": {
"I001": 1 "I001": 1
}, },
"tests/test_plugin_endpoint.py": {
"F401": 1,
"I001": 1
},
"tests/test_plugin_helper.py": { "tests/test_plugin_helper.py": {
"F401": 2, "F401": 2
"I001": 11
},
"tests/test_plugin_install_command.py": {
"I001": 1
}, },
"tests/test_plugin_lifecycle_status.py": { "tests/test_plugin_lifecycle_status.py": {
"F401": 1 "F401": 1
@@ -2039,9 +1824,6 @@
"tests/test_testing_bootstrap_plugin_namespace.py": { "tests/test_testing_bootstrap_plugin_namespace.py": {
"I001": 1 "I001": 1
}, },
"tests/test_tmdb_auxiliary.py": {
"F401": 1
},
"tests/test_tmdb_cache_management.py": { "tests/test_tmdb_cache_management.py": {
"I001": 1 "I001": 1
}, },
@@ -2133,16 +1915,9 @@
"tests/test_uvicorn_entrypoint.py": { "tests/test_uvicorn_entrypoint.py": {
"I001": 1 "I001": 1
}, },
"tests/test_web_agent_stream.py": {
"F401": 1,
"I001": 1
},
"tests/test_webpush_helper.py": { "tests/test_webpush_helper.py": {
"I001": 1 "I001": 1
}, },
"tests/test_workflow_actions.py": {
"I001": 1
},
"tests/test_workflow_authorization.py": { "tests/test_workflow_authorization.py": {
"I001": 1 "I001": 1
}, },
+4 -1
View File
@@ -4,7 +4,6 @@ from pathlib import Path
from ruamel.yaml import YAML from ruamel.yaml import YAML
PROJECT_ROOT = Path(__file__).parents[1] PROJECT_ROOT = Path(__file__).parents[1]
WORKFLOW_ROOT = PROJECT_ROOT / ".github" / "workflows" WORKFLOW_ROOT = PROJECT_ROOT / ".github" / "workflows"
@@ -33,6 +32,10 @@ def test_unit_test_workflow_has_independent_host_architecture_gate():
assert "tests/test_architecture_dependencies.py" in commands assert "tests/test_architecture_dependencies.py" in commands
assert "tests/test_architecture_contract_baseline.py" in commands assert "tests/test_architecture_contract_baseline.py" in commands
assert "scripts/architecture/baseline.py --check-host" in commands assert "scripts/architecture/baseline.py --check-host" in commands
assert "scripts/architecture/ruff_ratchet.py" in commands
assert "scripts/architecture/mypy_ratchet.py" in commands
assert "scripts/architecture/ruff_ratchet.py --write" not in commands
assert "scripts/architecture/mypy_ratchet.py --write" not in commands
assert "scripts/architecture/service_locator.py" in commands assert "scripts/architecture/service_locator.py" in commands
assert "scripts/startup/performance.py --check --repeat 3" in commands assert "scripts/startup/performance.py --check --repeat 3" in commands
+139 -6
View File
@@ -1,9 +1,13 @@
"""mypy 错误数只降不增 ratchet 的执行、解析与对比逻辑测试。""" """mypy 错误低水位 ratchet 的执行、解析与对比逻辑测试。"""
import json
import subprocess
import sys
from unittest.mock import patch from unittest.mock import patch
from scripts.architecture.mypy_ratchet import compare_counts, parse_errors, run_mypy import pytest
from scripts.architecture.mypy_ratchet import compare_counts, main, parse_errors, run_mypy
MYPY_SAMPLE = """ MYPY_SAMPLE = """
app/application/a.py:12: error: Function is missing a type annotation [no-untyped-def] app/application/a.py:12: error: Function is missing a type annotation [no-untyped-def]
@@ -18,13 +22,86 @@ Found 3 errors in 1 file (checked 500 source files)
def test_run_mypy_uses_stable_full_analysis() -> None: def test_run_mypy_uses_stable_full_analysis() -> None:
"""全量门禁不得复用前序检查缓存,也不得输出换行错误码。""" """全量门禁不得复用前序检查缓存,也不得输出换行错误码。"""
with patch("scripts.architecture.mypy_ratchet.subprocess.run") as run: with patch("scripts.architecture.mypy_ratchet.subprocess.run") as run:
run.return_value.stdout = "" run.return_value = subprocess.CompletedProcess(
args=[],
returncode=0,
stdout="Success: no issues found in 500 source files\n",
stderr="",
)
run_mypy() run_mypy()
command = run.call_args.args[0] command = run.call_args.args[0]
assert "--no-incremental" in command assert "--no-incremental" in command
assert "--no-pretty" in command assert "--no-pretty" in command
assert command[command.index("--platform") + 1] == "linux"
def test_run_mypy_rejects_tool_failure() -> None:
"""内部错误等退出码 2 必须让门禁失败,不能拿截断输出生成基线。"""
result = subprocess.CompletedProcess(
args=[],
returncode=2,
stdout=MYPY_SAMPLE,
stderr="mypy: INTERNAL ERROR",
)
with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result):
with pytest.raises(RuntimeError, match="异常退出.*INTERNAL ERROR"):
run_mypy()
def test_run_mypy_rejects_unexpected_stderr() -> None:
"""正常退出码携带 stderr 时仍须 fail-closed。"""
result = subprocess.CompletedProcess(
args=[],
returncode=1,
stdout=MYPY_SAMPLE,
stderr="analysis incomplete",
)
with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result):
with pytest.raises(RuntimeError, match="stderr"):
run_mypy()
def test_run_mypy_rejects_truncated_error_report() -> None:
"""摘要错误数与解析数量不同时必须拒绝不完整报告。"""
result = subprocess.CompletedProcess(
args=[],
returncode=1,
stdout=MYPY_SAMPLE.replace("Found 3 errors", "Found 4 errors"),
stderr="",
)
with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result):
with pytest.raises(RuntimeError, match="摘要与已解析错误数不一致"):
run_mypy()
def test_run_mypy_accepts_complete_error_report() -> None:
"""退出码 1 的完整诊断应交给 ratchet 比较,而不是被当作工具失败。"""
result = subprocess.CompletedProcess(
args=[],
returncode=1,
stdout=MYPY_SAMPLE,
stderr="",
)
with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result):
assert run_mypy() == MYPY_SAMPLE
def test_run_mypy_rejects_missing_summary() -> None:
"""没有完成摘要的错误输出可能被截断,必须拒绝。"""
result = subprocess.CompletedProcess(
args=[],
returncode=1,
stdout=MYPY_SAMPLE.replace(
"Found 3 errors in 1 file (checked 500 source files)\n",
"",
),
stderr="",
)
with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result):
with pytest.raises(RuntimeError, match="缺少唯一的完整摘要"):
run_mypy()
def test_parse_errors_aggregates_per_file_and_code() -> None: def test_parse_errors_aggregates_per_file_and_code() -> None:
@@ -46,12 +123,24 @@ def test_parse_errors_buckets_missing_code_as_unknown() -> None:
assert report == {"app/b.py": {"unknown": 1}} assert report == {"app/b.py": {"unknown": 1}}
def test_ratchet_allows_removal_and_reduction() -> None: def test_ratchet_requires_reduced_errors_to_be_persisted() -> None:
"""文件删除或错误减少必须允许通过""" """错误减少必须刷新 fixture,避免旧额度允许回退"""
baseline = {"app/a.py": {"arg-type": 5, "misc": 2}} baseline = {"app/a.py": {"arg-type": 5, "misc": 2}}
current = {"app/a.py": {"arg-type": 3}} current = {"app/a.py": {"arg-type": 3}}
assert compare_counts(baseline, current) == [] assert compare_counts(baseline, current) == [
"app/a.py: 类型错误低水位未固化 [arg-type] 5->3",
"app/a.py: 类型错误低水位未固化 [misc] 2->0",
]
def test_ratchet_requires_deleted_file_to_be_persisted() -> None:
"""整文件债务消失也必须写回 fixture。"""
baseline = {"app/a.py": {"arg-type": 1}}
assert compare_counts(baseline, {}) == [
"app/a.py: 类型错误低水位未固化 [arg-type] 1->0"
]
def test_ratchet_rejects_growth_and_new_errors() -> None: def test_ratchet_rejects_growth_and_new_errors() -> None:
@@ -71,3 +160,47 @@ def test_ratchet_rejects_growth_and_new_errors() -> None:
assert any("app/a.py" in p and "arg-type" in p and "既有错误增长" in p for p in problems) assert any("app/a.py" in p and "arg-type" in p and "既有错误增长" in p for p in problems)
assert any("app/a.py" in p and "type-arg" in p and "新增类型错误" in p for p in problems) assert any("app/a.py" in p and "type-arg" in p and "新增类型错误" in p for p in problems)
assert any("app/b.py" in p and "新增类型错误" in p for p in problems) assert any("app/b.py" in p and "新增类型错误" in p for p in problems)
def test_write_refuses_to_legalize_type_regression(tmp_path, monkeypatch) -> None:
"""已有基线出现增长时,--write 不得覆盖原文件。"""
baseline_path = tmp_path / "mypy-baseline.json"
baseline_path.write_text("{}\n", encoding="utf-8")
monkeypatch.setattr(
sys,
"argv",
["mypy_ratchet.py", "--write", "--baseline", str(baseline_path)],
)
with patch("scripts.architecture.mypy_ratchet.run_mypy", return_value=MYPY_SAMPLE):
assert main() == 1
assert json.loads(baseline_path.read_text(encoding="utf-8")) == {}
def test_write_persists_reduced_type_low_watermark(tmp_path, monkeypatch) -> None:
"""没有增长时,--write 应把下降后的完整快照固化。"""
baseline_path = tmp_path / "mypy-baseline.json"
baseline_path.write_text(
json.dumps(
{
"app/application/a.py": {
"no-untyped-def": 3,
"type-arg": 1,
}
}
),
encoding="utf-8",
)
monkeypatch.setattr(
sys,
"argv",
["mypy_ratchet.py", "--write", "--baseline", str(baseline_path)],
)
with patch("scripts.architecture.mypy_ratchet.run_mypy", return_value=MYPY_SAMPLE):
assert main() == 0
assert json.loads(baseline_path.read_text(encoding="utf-8")) == {
"app/application/a.py": {"no-untyped-def": 2, "type-arg": 1}
}
+109 -6
View File
@@ -1,29 +1,50 @@
"""Ruff 与覆盖率增量门禁测试。""" """Ruff 与覆盖率增量门禁测试。"""
import json
import subprocess
import sys
from unittest.mock import patch
import pytest
from scripts.architecture.coverage_ratchet import ( from scripts.architecture.coverage_ratchet import (
collect_package_coverage, collect_package_coverage,
compare_coverage, compare_coverage,
) )
from scripts.architecture.ruff_ratchet import aggregate_diagnostics, compare_counts from scripts.architecture.ruff_ratchet import (
PROJECT_ROOT,
aggregate_diagnostics,
compare_counts,
run_ruff,
)
from scripts.architecture.ruff_ratchet import (
main as ruff_main,
)
def test_ruff_ratchet_rejects_new_rule_and_count_growth() -> None: def test_ruff_ratchet_rejects_new_rule_and_count_growth() -> None:
"""Ruff 基线允许修复,但拒绝新增规则和既有数量增长""" """Ruff 基线拒绝新增、增长以及尚未固化的下降"""
baseline = {"app/example.py": {"F401": 2}} baseline = {"app/example.py": {"F401": 2}}
assert compare_counts(baseline, {"app/example.py": {"F401": 1}}) == [] assert compare_counts(baseline, {"app/example.py": {"F401": 1}}) == [
"app/example.py: Ruff 低水位未固化 [F401] 2->1"
]
assert compare_counts(baseline, {"app/example.py": {"F401": 3}}) == [ assert compare_counts(baseline, {"app/example.py": {"F401": 3}}) == [
"app/example.py: Ruff 诊断增长 [F401] 2->3" "app/example.py: Ruff 诊断增长 [F401] 2->3"
] ]
assert compare_counts(baseline, {"app/new.py": {"I001": 1}}) == [ assert compare_counts(
baseline,
{
"app/example.py": {"F401": 2},
"app/new.py": {"I001": 1},
},
) == [
"app/new.py: 新增 Ruff 诊断 [I001] x1" "app/new.py: 新增 Ruff 诊断 [I001] x1"
] ]
def test_ruff_diagnostics_are_aggregated_by_relative_file_and_code() -> None: def test_ruff_diagnostics_are_aggregated_by_relative_file_and_code() -> None:
"""Ruff JSON 的绝对路径必须归一化为稳定仓库路径。""" """Ruff JSON 的绝对路径必须归一化为稳定仓库路径。"""
from scripts.architecture.ruff_ratchet import PROJECT_ROOT
path = PROJECT_ROOT / "app" / "example.py" path = PROJECT_ROOT / "app" / "example.py"
diagnostics = [ diagnostics = [
{"filename": str(path), "code": "F401"}, {"filename": str(path), "code": "F401"},
@@ -36,6 +57,88 @@ def test_ruff_diagnostics_are_aggregated_by_relative_file_and_code() -> None:
} }
def test_run_ruff_rejects_tool_failure() -> None:
"""Ruff 工具异常必须 fail-closed,不能把空输出当成零诊断。"""
result = subprocess.CompletedProcess(
args=[],
returncode=2,
stdout="",
stderr="ruff failed",
)
with patch("scripts.architecture.ruff_ratchet.subprocess.run", return_value=result):
with pytest.raises(RuntimeError, match="ruff failed"):
run_ruff()
@pytest.mark.parametrize(
("returncode", "stdout", "message"),
[
(0, '[{"code": "F401"}]', "成功但仍输出诊断"),
(1, "[]", "诊断状态但输出为空"),
],
)
def test_run_ruff_rejects_status_payload_mismatch(
returncode: int,
stdout: str,
message: str,
) -> None:
"""Ruff 退出状态必须与结构化诊断是否存在一致。"""
result = subprocess.CompletedProcess(
args=[],
returncode=returncode,
stdout=stdout,
stderr="",
)
with patch("scripts.architecture.ruff_ratchet.subprocess.run", return_value=result):
with pytest.raises(RuntimeError, match=message):
run_ruff()
def test_ruff_write_refuses_to_legalize_regression(tmp_path, monkeypatch) -> None:
"""已有基线出现增长时,--write 不得覆盖原文件。"""
baseline_path = tmp_path / "ruff-baseline.json"
baseline_path.write_text("{}\n", encoding="utf-8")
monkeypatch.setattr(
sys,
"argv",
["ruff_ratchet.py", "--write", "--baseline", str(baseline_path)],
)
diagnostic = {
"filename": str(PROJECT_ROOT / "app" / "example.py"),
"code": "F401",
}
with patch("scripts.architecture.ruff_ratchet.run_ruff", return_value=[diagnostic]):
assert ruff_main() == 1
assert json.loads(baseline_path.read_text(encoding="utf-8")) == {}
def test_ruff_write_persists_reduced_low_watermark(tmp_path, monkeypatch) -> None:
"""没有增长时,--write 应把下降后的 Ruff 快照固化。"""
baseline_path = tmp_path / "ruff-baseline.json"
baseline_path.write_text(
json.dumps({"app/example.py": {"F401": 2}}),
encoding="utf-8",
)
monkeypatch.setattr(
sys,
"argv",
["ruff_ratchet.py", "--write", "--baseline", str(baseline_path)],
)
diagnostic = {
"filename": str(PROJECT_ROOT / "app" / "example.py"),
"code": "F401",
}
with patch("scripts.architecture.ruff_ratchet.run_ruff", return_value=[diagnostic]):
assert ruff_main() == 0
assert json.loads(baseline_path.read_text(encoding="utf-8")) == {
"app/example.py": {"F401": 1}
}
def test_coverage_ratchet_aggregates_governed_packages() -> None: def test_coverage_ratchet_aggregates_governed_packages() -> None:
"""覆盖率门禁只聚合 Application 与 Domain,并按真实语句数加权。""" """覆盖率门禁只聚合 Application 与 Domain,并按真实语句数加权。"""
report = { report = {
+30 -7
View File
@@ -71,22 +71,45 @@ class TestSystemNotificationDispatch(unittest.TestCase):
def test_async_send_message_uses_executor_for_immediate_send(self): def test_async_send_message_uses_executor_for_immediate_send(self):
"""异步立即发送不能在事件循环里直接执行同步渠道回调。""" """异步立即发送不能在事件循环里直接执行同步渠道回调。"""
class _FakeLoop: class _FakeLoop:
def __init__(self): """同步模拟 asyncio 的 executor 提交,并记录提交时的上下文。"""
self.called = False
async def run_in_executor(self, _executor, func, *args): def __init__(self, loop):
self.called = False
self.loop = loop
self.submission_correlation_id = None
def run_in_executor(self, _executor, func, *args):
"""立即执行 worker 调用并返回已完成 Future。"""
self.called = True self.called = True
func(*args) self.submission_correlation_id = get_correlation_id()
future = self.loop.create_future()
try:
func(*args)
except BaseException as error:
future.set_exception(error)
else:
future.set_result(None)
return future
async def _run(): async def _run():
manager = MessageQueueManager() manager = MessageQueueManager()
fake_loop = _FakeLoop() fake_loop = _FakeLoop(asyncio.get_running_loop())
observed = []
with patch( with patch(
"asyncio.get_running_loop", "asyncio.get_running_loop",
return_value=fake_loop, return_value=fake_loop,
), patch.object(manager, "_send") as send: ), patch.object(
await manager.async_send_message("payload", immediately=True) manager,
"_send",
side_effect=lambda *_args, **_kwargs: observed.append(
get_correlation_id()
),
) as send:
with correlation_scope("message-request"):
await manager.async_send_message("payload", immediately=True)
self.assertTrue(fake_loop.called) self.assertTrue(fake_loop.called)
self.assertIsNone(fake_loop.submission_correlation_id)
self.assertEqual(observed, ["message-request"])
send.assert_called_once_with("payload") send.assert_called_once_with("payload")
asyncio.run(_run()) asyncio.run(_run())