mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
fix(quality): close type and lint ratchets
This commit is contained in:
@@ -963,13 +963,9 @@ class MessageQueueManager(metaclass=SingletonClass):
|
||||
context = copy_context()
|
||||
call = partial(self._send, *args, **kwargs)
|
||||
loop = asyncio.get_running_loop()
|
||||
submission = partial(loop.run_in_executor, None, context.run, call)
|
||||
# 默认执行器保持空底层上下文,渠道调用只使用当前消息快照。
|
||||
await Context().run(
|
||||
loop.run_in_executor,
|
||||
None,
|
||||
context.run,
|
||||
call,
|
||||
)
|
||||
await Context().run(submission)
|
||||
return
|
||||
self.queue.put({
|
||||
"args": args,
|
||||
|
||||
@@ -83,8 +83,8 @@
|
||||
|
||||
| 批次 | 叶子目标 | 状态 | 当前证据/停止条件 |
|
||||
|---|---|---|---|
|
||||
| 0 | 历史任务清账、现行架构图、外部契约核对和宿主基线对齐 | 已本地验证 | #6472 遗漏的 dependency fixture 和 Schema 导出清单已对齐;架构契约 `71 passed` |
|
||||
| 1 | Mypy fail-closed,并把 Ruff/Mypy 已下降债务固化为真实低水位 | 待批次 0 | Mypy 1.18.2 在 `message.py` 内部错误退出 2,但脚本误报通过;Ruff 978 对旧基线 1623 |
|
||||
| 0 | 历史任务清账、现行架构图、外部契约核对和宿主基线对齐 | 已推送 | `d234c7132`;远端同 SHA;ahead/behind `0/0`;架构契约 `71 passed` |
|
||||
| 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 仅作参考,必须在最终代码快照本地重建 |
|
||||
| 3 | 收口阶段 62 遗留的 QQ Gateway heartbeat Timer 所有权 | 待批次 2 | Timer 只 cancel 不 join,Gateway 主线程可能在 heartbeat 仍执行时报告停止成功 |
|
||||
| Final | 全仓回归、插件兼容复核、台账定稿和远端一致性验证 | 待前置批次 | 所有准入项已推送;全量测试和适用门禁通过;本地/远端 0/0 |
|
||||
@@ -106,7 +106,8 @@
|
||||
* `baseline.py --check-host`、`scripts/schema/exports.py --check` 和相关架构契约测试通过,
|
||||
测试结果为 `71 passed`。
|
||||
|
||||
待完成:提交并推送本批次,记录远端证据。
|
||||
交付证据:提交 `d234c7132` 已推送到 `origin/v3`;`git ls-remote` 返回同一 SHA,
|
||||
`HEAD...origin/v3` 为 `0/0`,且本地 HEAD 是远端祖先。
|
||||
|
||||
### 批次 1:Ruff/Mypy 低水位闭环
|
||||
|
||||
@@ -121,6 +122,20 @@
|
||||
完整扫描正常退出并生成真实 Mypy fixture;Ruff/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 收紧到 976,Mypy 和 Ruff 默认路径复跑均通过;
|
||||
* 质量/CI/上下文/严格类型专项 `33 passed`,架构契约 `71 passed`,改动文件 Pylint `10.00/10`。
|
||||
|
||||
待完成:提交推送并记录远端证据。
|
||||
|
||||
### 批次 2:Coverage 低水位闭环
|
||||
|
||||
已确认的既有缺口: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 生成契约 | dependency fixture 与 Schema 导出清单已更新;架构契约 `71 passed` |
|
||||
| 2026-08-26 | 后台 owner 历史余项审计 | 仅 QQ heartbeat Timer 满足阶段 62 既有合同和两天准入条件,其余候选关闭或排除 |
|
||||
| 2026-08-26 | 批次 0 交付 | `d234c7132` 已推送;远端同 SHA;ahead/behind `0/0` |
|
||||
| 2026-08-26 | 批次 1 本地验收 | Mypy 11994、Ruff 976;专项 `33 passed`;架构契约 `71 passed`;Pylint `10.00/10` |
|
||||
|
||||
## 七、本轮停止条件
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""为宿主源码维护 mypy 类型错误只降不增的基线。
|
||||
|
||||
与 ``complexity.py`` 同一模式:AST/subprocess 收集当前计数,与 JSON 基线对比,
|
||||
删除和减少均合法,新增文件或错误码、既有计数增长都会被拒绝。
|
||||
基线按 文件 -> 错误码 -> 数量 三级组织,修复单个文件后可用 ``--write`` 收紧基线,
|
||||
不影响其他文件的存量豁免。
|
||||
基线按 文件 -> 错误码 -> 数量 三级组织。任何增长都会被拒绝;债务下降后也必须用
|
||||
``--write`` 固化新的低水位,避免已经修复的错误重新获得回退额度。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,19 +21,23 @@ DEFAULT_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/mypy-baseline.jso
|
||||
# 只扫描宿主源码;app/plugins 是运行时插件副本,质量由插件市场链路自行管理。
|
||||
MYPY_TARGETS = ("app",)
|
||||
MYPY_EXCLUDES = ("app/plugins",)
|
||||
MYPY_PLATFORM = "linux"
|
||||
|
||||
# 形如 app/foo.py:12: error: 消息说明 [error-code];个别错误可能缺代码。
|
||||
_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:
|
||||
"""以当前解释器运行全量 mypy 并返回 stdout(非零退出码属于预期结果)。"""
|
||||
"""运行全量 mypy;只接受可完整解析的正常成功或诊断结果。"""
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"mypy",
|
||||
"--no-incremental",
|
||||
"--no-pretty",
|
||||
"--platform",
|
||||
MYPY_PLATFORM,
|
||||
*MYPY_TARGETS,
|
||||
]
|
||||
for pattern in MYPY_EXCLUDES:
|
||||
@@ -46,6 +49,36 @@ def run_mypy() -> str:
|
||||
text=True,
|
||||
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
|
||||
|
||||
|
||||
@@ -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())}
|
||||
|
||||
|
||||
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(
|
||||
baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]]
|
||||
) -> list[str]:
|
||||
"""返回新增文件/错误码或既有计数增长问题;删除与减少均合法。"""
|
||||
problems = []
|
||||
for path, codes in current.items():
|
||||
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
|
||||
"""返回类型错误增长和尚未固化的新低水位。"""
|
||||
regressions, stale = classify_counts(baseline, current)
|
||||
return [*regressions, *stale]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -88,22 +140,40 @@ def main() -> int:
|
||||
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
|
||||
args = parser.parse_args()
|
||||
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 baseline_exists and regressions:
|
||||
print("\n".join(regressions))
|
||||
print("拒绝写入:当前结果包含类型错误回退,--write 只能固化下降后的低水位。")
|
||||
return 1
|
||||
args.baseline.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.baseline.write_text(
|
||||
json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
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
|
||||
baseline = json.loads(args.baseline.read_text(encoding="utf-8"))
|
||||
problems = compare_counts(baseline, current)
|
||||
problems = [*regressions, *stale]
|
||||
if problems:
|
||||
print("\n".join(problems))
|
||||
print("提示:修复后可用 --write 收紧基线;禁止为绕过门禁放宽基线。")
|
||||
if regressions:
|
||||
print("先消除类型错误回退;存在增长时禁止用 --write 覆盖基线。")
|
||||
else:
|
||||
print("提示:当前只有债务下降,可用 --write 固化新的低水位。")
|
||||
return 1
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,14 @@ def run_ruff() -> list[dict[str, Any]]:
|
||||
)
|
||||
if result.returncode not in {0, 1}:
|
||||
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(
|
||||
@@ -40,22 +47,40 @@ def aggregate_diagnostics(
|
||||
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(
|
||||
baseline: dict[str, dict[str, int]],
|
||||
current: dict[str, dict[str, int]],
|
||||
) -> list[str]:
|
||||
"""返回新增规则或既有数量增长;修复和删除均合法。"""
|
||||
problems = []
|
||||
for path, codes in current.items():
|
||||
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
|
||||
"""返回 Ruff 诊断增长和尚未固化的新低水位。"""
|
||||
regressions, stale = classify_counts(baseline, current)
|
||||
return [*regressions, *stale]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -65,22 +90,40 @@ def main() -> int:
|
||||
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
|
||||
args = parser.parse_args()
|
||||
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 baseline_exists and regressions:
|
||||
print("\n".join(regressions))
|
||||
print("拒绝写入:当前结果包含 Ruff 回退,--write 只能固化下降后的低水位。")
|
||||
return 1
|
||||
args.baseline.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.baseline.write_text(
|
||||
json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
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
|
||||
baseline = json.loads(args.baseline.read_text(encoding="utf-8"))
|
||||
problems = compare_counts(baseline, current)
|
||||
problems = [*regressions, *stale]
|
||||
if problems:
|
||||
print("\n".join(problems))
|
||||
print("提示:修复后可用 --write 收紧基线;禁止为绕过门禁放宽基线。")
|
||||
if regressions:
|
||||
print("先消除 Ruff 回退;存在增长时禁止用 --write 覆盖基线。")
|
||||
else:
|
||||
print("提示:当前只有债务下降,可用 --write 固化新的低水位。")
|
||||
return 1
|
||||
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
|
||||
|
||||
|
||||
|
||||
+1870
-36
File diff suppressed because it is too large
Load Diff
+30
-255
@@ -1,10 +1,4 @@
|
||||
{
|
||||
"app/adapters/cache/backends.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/adapters/cache/redis.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/adapters/external/cookiecloud.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -27,9 +21,6 @@
|
||||
"app/adapters/system/display/__init__.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/adapters/system/fsproxy.py": {
|
||||
"E402": 1
|
||||
},
|
||||
"app/adapters/system/host.py": {
|
||||
"I001": 2
|
||||
},
|
||||
@@ -39,9 +30,6 @@
|
||||
"app/adapters/system/plugin/package.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/adapters/system/resource.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/adapters/system/rust.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -62,14 +50,9 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/llm/capability.py": {
|
||||
"E402": 2,
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/llm/helper.py": {
|
||||
"E402": 1
|
||||
},
|
||||
"app/agent/llm/provider.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/llm/server_tools.py": {
|
||||
@@ -78,9 +61,6 @@
|
||||
"app/agent/mcp.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/memory/__init__.py": {
|
||||
"E402": 4
|
||||
},
|
||||
"app/agent/middleware/memory.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -100,8 +80,8 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/orchestrator.py": {
|
||||
"E402": 14,
|
||||
"I001": 3
|
||||
"E402": 11,
|
||||
"I001": 2
|
||||
},
|
||||
"app/agent/policy/__init__.py": {
|
||||
"I001": 1
|
||||
@@ -119,7 +99,6 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/prompt/__init__.py": {
|
||||
"E402": 6,
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/runtime.py": {
|
||||
@@ -131,13 +110,8 @@
|
||||
"app/agent/skills/metadata.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/skills/registry.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/base.py": {
|
||||
"E402": 5,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/factory.py": {
|
||||
"I001": 1
|
||||
@@ -149,12 +123,8 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/_plugin_tool_utils.py": {
|
||||
"E402": 8,
|
||||
"I001": 3
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py": {
|
||||
"E402": 1
|
||||
},
|
||||
"app/agent/tools/impl/_torrent_search_utils.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -162,7 +132,6 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/add_download_tasks.py": {
|
||||
"E402": 7,
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/add_subscribe.py": {
|
||||
@@ -175,7 +144,6 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/create_agent_task.py": {
|
||||
"E402": 3,
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/delete_custom_filter_rule.py": {
|
||||
@@ -202,9 +170,6 @@
|
||||
"app/agent/tools/impl/list_directory.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/query_agent_tasks.py": {
|
||||
"E402": 1
|
||||
},
|
||||
"app/agent/tools/impl/query_builtin_filter_rules.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -260,7 +225,6 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/recognize_media.py": {
|
||||
"E402": 6,
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/reload_plugin.py": {
|
||||
@@ -270,7 +234,6 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/scrape_metadata.py": {
|
||||
"E402": 6,
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/search_media.py": {
|
||||
@@ -285,14 +248,10 @@
|
||||
"app/agent/tools/impl/search_torrents.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/search_web.py": {
|
||||
"E402": 1
|
||||
},
|
||||
"app/agent/tools/impl/send_local_file.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/send_voice_message.py": {
|
||||
"E402": 3,
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/test_site.py": {
|
||||
@@ -304,9 +263,6 @@
|
||||
"app/agent/tools/impl/uninstall_plugin.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/agent/tools/impl/update_agent_task.py": {
|
||||
"E402": 2
|
||||
},
|
||||
"app/agent/tools/impl/update_custom_filter_rule.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -398,12 +354,6 @@
|
||||
"app/application/outbox.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/application/plugin/catalog.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/application/plugin/install.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/application/plugin/runtime.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -470,10 +420,6 @@
|
||||
"app/chain/transfer.py": {
|
||||
"E402": 21
|
||||
},
|
||||
"app/cli.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/command.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -486,9 +432,6 @@
|
||||
"app/db/adapters/outbox.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/db/adapters/site.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/db/adapters/transaction.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -597,10 +540,6 @@
|
||||
"app/doctor/__init__.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/doctor/checks.py": {
|
||||
"E402": 3,
|
||||
"I001": 1
|
||||
},
|
||||
"app/doctor/dependencies.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -608,12 +547,10 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/doctor/runner.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/domain/context.py": {
|
||||
"E731": 1,
|
||||
"I001": 1
|
||||
"E731": 1
|
||||
},
|
||||
"app/domain/media.py": {
|
||||
"E731": 1
|
||||
@@ -660,10 +597,6 @@
|
||||
"app/domain/title.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/factory.py": {
|
||||
"E402": 9,
|
||||
"I001": 2
|
||||
},
|
||||
"app/foundation/collections.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -693,23 +626,18 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/acoustid/__init__.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/anilist/__init__.py": {
|
||||
"E402": 8,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/anilist/anilist.py": {
|
||||
"E402": 2,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/bangumi/__init__.py": {
|
||||
"E402": 9,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/bangumi/bangumi.py": {
|
||||
"E402": 1,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/discord/__init__.py": {
|
||||
@@ -717,17 +645,14 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/discord/discord.py": {
|
||||
"E402": 6,
|
||||
"E741": 1,
|
||||
"F541": 2,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/douban/__init__.py": {
|
||||
"E402": 16,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/douban/apiv2.py": {
|
||||
"E402": 2,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/douban/scraper.py": {
|
||||
@@ -738,66 +663,36 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/emby/emby.py": {
|
||||
"E402": 6,
|
||||
"F541": 33,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/fanart/__init__.py": {
|
||||
"E402": 5,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/feishu/__init__.py": {
|
||||
"F401": 1,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/feishu/feishu.py": {
|
||||
"E402": 10,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/filemanager/__init__.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/filemanager/module.py": {
|
||||
"E402": 20,
|
||||
"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
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/filemanager/transhandler.py": {
|
||||
"E402": 21,
|
||||
"F541": 2,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/filter/__init__.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/imdb/__init__.py": {
|
||||
"E402": 5
|
||||
},
|
||||
"app/modules/imdb/api.py": {
|
||||
"E402": 1
|
||||
},
|
||||
"app/modules/indexer/__init__.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/parser/__init__.py": {
|
||||
"E402": 5,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/parser/bitpt.py": {
|
||||
@@ -810,9 +705,6 @@
|
||||
"app/modules/indexer/parser/file_list.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/parser/gazelle.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/parser/hddolby.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -837,7 +729,6 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/parser/rousi.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/parser/small_horse.py": {
|
||||
@@ -862,40 +753,31 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/spider/__init__.py": {
|
||||
"E402": 8,
|
||||
"F541": 1,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/spider/haidan.py": {
|
||||
"E402": 6,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/spider/hddolby.py": {
|
||||
"E402": 5,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/spider/mtorrent.py": {
|
||||
"E402": 6,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/spider/rousi.py": {
|
||||
"E402": 6,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/spider/sunnypt.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/spider/tnode.py": {
|
||||
"E402": 4,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/spider/torrentleech.py": {
|
||||
"E402": 5,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/indexer/spider/yema.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/jellyfin/__init__.py": {
|
||||
@@ -903,24 +785,19 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/jellyfin/jellyfin.py": {
|
||||
"E402": 7,
|
||||
"F541": 34,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/listenbrainz/__init__.py": {
|
||||
"E402": 5,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/lrclib/__init__.py": {
|
||||
"E402": 6,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/musicbrainz/__init__.py": {
|
||||
"E402": 10,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/musicbrainz/music_cache.py": {
|
||||
"E402": 5,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/navidrome/__init__.py": {
|
||||
@@ -953,10 +830,6 @@
|
||||
"app/modules/qqbot/module.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/qqbot/qqbot.py": {
|
||||
"E402": 9,
|
||||
"I001": 2
|
||||
},
|
||||
"app/modules/rtorrent/__init__.py": {
|
||||
"F541": 2,
|
||||
"I001": 1
|
||||
@@ -970,11 +843,9 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/slack/slack.py": {
|
||||
"E402": 6,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/subtitle/__init__.py": {
|
||||
"E402": 7,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/synologychat/__init__.py": {
|
||||
@@ -996,46 +867,32 @@
|
||||
"I001": 3
|
||||
},
|
||||
"app/modules/theaudiodb/__init__.py": {
|
||||
"E402": 8,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/themoviedb/__init__.py": {
|
||||
"E402": 15,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/themoviedb/category.py": {
|
||||
"E402": 3,
|
||||
"F541": 1,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/themoviedb/scraper.py": {
|
||||
"E402": 5,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/themoviedb/tmdb_cache.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/themoviedb/tmdbapi.py": {
|
||||
"E402": 6,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/__init__.py": {
|
||||
"F401": 25
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/discover.py": {
|
||||
"E402": 1
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/tmdb.py": {
|
||||
"E402": 2,
|
||||
"I001": 2
|
||||
},
|
||||
"app/modules/thetvdb/__init__.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/thetvdb/tvdb_v4_official.py": {
|
||||
"E402": 1
|
||||
"app/modules/thetvdb/__init__.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/transmission/__init__.py": {
|
||||
"F541": 2,
|
||||
@@ -1048,7 +905,6 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/trimemedia/api.py": {
|
||||
"E402": 2,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/trimemedia/module.py": {
|
||||
@@ -1077,8 +933,7 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/webpush/__init__.py": {
|
||||
"E402": 4,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/wechat/__init__.py": {
|
||||
"F541": 3,
|
||||
@@ -1088,15 +943,10 @@
|
||||
"F541": 1,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/wechat/wechatbot.py": {
|
||||
"E402": 10,
|
||||
"I001": 2
|
||||
},
|
||||
"app/modules/wechatclawbot/__init__.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/wechatclawbot/wechatclawbot.py": {
|
||||
"E402": 6,
|
||||
"I001": 1
|
||||
},
|
||||
"app/modules/zspace/__init__.py": {
|
||||
@@ -1149,14 +999,10 @@
|
||||
"app/runtime/event/errors.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/events.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/execution.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/extensions/host_module_adapter.py": {
|
||||
"E402": 2,
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/extensions/managed_resource_adapter.py": {
|
||||
@@ -1166,12 +1012,8 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/extensions/module_manager.py": {
|
||||
"E402": 4,
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/extensions/plugin/catalog.py": {
|
||||
"E402": 5
|
||||
},
|
||||
"app/runtime/extensions/plugin/lifecycle.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -1182,8 +1024,7 @@
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/extensions/plugin_manager.py": {
|
||||
"E402": 21,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/extensions/service_config.py": {
|
||||
"I001": 1
|
||||
@@ -1207,12 +1048,8 @@
|
||||
"app/runtime/reload.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/settings.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/state.py": {
|
||||
"E402": 3,
|
||||
"I001": 2
|
||||
"I001": 1
|
||||
},
|
||||
"app/schemas/agent.py": {
|
||||
"I001": 1
|
||||
@@ -1223,9 +1060,6 @@
|
||||
"app/schemas/context.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/schemas/event.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/schemas/file.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -1271,9 +1105,6 @@
|
||||
"app/sdk/config.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/sdk/events.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/sdk/logging.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -1292,21 +1123,6 @@
|
||||
"app/sdk/utilities.py": {
|
||||
"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": {
|
||||
"E402": 27,
|
||||
"F401": 1
|
||||
@@ -1314,9 +1130,6 @@
|
||||
"app/workflow/__init__.py": {
|
||||
"F401": 1
|
||||
},
|
||||
"scripts/architecture/baseline.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"scripts/architecture/task_ownership.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -1432,9 +1245,6 @@
|
||||
"tests/test_agent_summarization_streaming.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_agent_task_run_migration.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_agent_task_runs.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -1465,9 +1275,6 @@
|
||||
"tests/test_api_response.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_architecture_ci.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_architecture_contract_baseline.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -1605,10 +1412,6 @@
|
||||
"tests/test_episode_group_recognition.py": {
|
||||
"E402": 4
|
||||
},
|
||||
"tests/test_event_contracts.py": {
|
||||
"F401": 1,
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_event_dispatch_snapshot.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -1662,12 +1465,8 @@
|
||||
"tests/test_legacy_import_compat.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_legacy_plugin_resource_imports.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_lifecycle_shutdown.py": {
|
||||
"F841": 1,
|
||||
"I001": 1
|
||||
"F841": 1
|
||||
},
|
||||
"tests/test_llm_provider_bedrock.py": {
|
||||
"I001": 1
|
||||
@@ -1777,9 +1576,6 @@
|
||||
"tests/test_module_manager_capability_adapter.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_module_method_contracts.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_module_quality.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -1838,9 +1634,6 @@
|
||||
"tests/test_musicbrainz_module.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_mypy_gate.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_navidrome_module.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -1868,16 +1661,8 @@
|
||||
"tests/test_plugin_dependency_service.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_plugin_endpoint.py": {
|
||||
"F401": 1,
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_plugin_helper.py": {
|
||||
"F401": 2,
|
||||
"I001": 11
|
||||
},
|
||||
"tests/test_plugin_install_command.py": {
|
||||
"I001": 1
|
||||
"F401": 2
|
||||
},
|
||||
"tests/test_plugin_lifecycle_status.py": {
|
||||
"F401": 1
|
||||
@@ -2039,9 +1824,6 @@
|
||||
"tests/test_testing_bootstrap_plugin_namespace.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_tmdb_auxiliary.py": {
|
||||
"F401": 1
|
||||
},
|
||||
"tests/test_tmdb_cache_management.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -2133,16 +1915,9 @@
|
||||
"tests/test_uvicorn_entrypoint.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_web_agent_stream.py": {
|
||||
"F401": 1,
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_webpush_helper.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_workflow_actions.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_workflow_authorization.py": {
|
||||
"I001": 1
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ from pathlib import Path
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parents[1]
|
||||
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_contract_baseline.py" 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/startup/performance.py --check --repeat 3" in commands
|
||||
|
||||
|
||||
+139
-6
@@ -1,9 +1,13 @@
|
||||
"""mypy 错误数只降不增 ratchet 的执行、解析与对比逻辑测试。"""
|
||||
"""mypy 错误低水位 ratchet 的执行、解析与对比逻辑测试。"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
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 = """
|
||||
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:
|
||||
"""全量门禁不得复用前序检查缓存,也不得输出换行错误码。"""
|
||||
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()
|
||||
|
||||
command = run.call_args.args[0]
|
||||
assert "--no-incremental" 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:
|
||||
@@ -46,12 +123,24 @@ def test_parse_errors_buckets_missing_code_as_unknown() -> None:
|
||||
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}}
|
||||
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:
|
||||
@@ -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 "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)
|
||||
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,50 @@
|
||||
"""Ruff 与覆盖率增量门禁测试。"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.architecture.coverage_ratchet import (
|
||||
collect_package_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:
|
||||
"""Ruff 基线允许修复,但拒绝新增规则和既有数量增长。"""
|
||||
"""Ruff 基线拒绝新增、增长以及尚未固化的下降。"""
|
||||
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}}) == [
|
||||
"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"
|
||||
]
|
||||
|
||||
|
||||
def test_ruff_diagnostics_are_aggregated_by_relative_file_and_code() -> None:
|
||||
"""Ruff JSON 的绝对路径必须归一化为稳定仓库路径。"""
|
||||
from scripts.architecture.ruff_ratchet import PROJECT_ROOT
|
||||
|
||||
path = PROJECT_ROOT / "app" / "example.py"
|
||||
diagnostics = [
|
||||
{"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:
|
||||
"""覆盖率门禁只聚合 Application 与 Domain,并按真实语句数加权。"""
|
||||
report = {
|
||||
|
||||
@@ -71,22 +71,45 @@ class TestSystemNotificationDispatch(unittest.TestCase):
|
||||
def test_async_send_message_uses_executor_for_immediate_send(self):
|
||||
"""异步立即发送不能在事件循环里直接执行同步渠道回调。"""
|
||||
class _FakeLoop:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
"""同步模拟 asyncio 的 executor 提交,并记录提交时的上下文。"""
|
||||
|
||||
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.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():
|
||||
manager = MessageQueueManager()
|
||||
fake_loop = _FakeLoop()
|
||||
fake_loop = _FakeLoop(asyncio.get_running_loop())
|
||||
observed = []
|
||||
with patch(
|
||||
"asyncio.get_running_loop",
|
||||
return_value=fake_loop,
|
||||
), patch.object(manager, "_send") as send:
|
||||
), patch.object(
|
||||
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.assertIsNone(fake_loop.submission_correlation_id)
|
||||
self.assertEqual(observed, ["message-request"])
|
||||
send.assert_called_once_with("payload")
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
Reference in New Issue
Block a user