debug-control-plane 0.5.0__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/PKG-INFO +4 -4
  2. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/README.md +3 -3
  3. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/__init__.py +1 -1
  4. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/mcp_plane/server.py +1 -1
  5. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane.egg-info/PKG-INFO +4 -4
  6. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane.egg-info/SOURCES.txt +1 -0
  7. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/pyproject.toml +1 -1
  8. debug_control_plane-0.5.2/tests/test_dart_plane_persistence.py +217 -0
  9. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_server.py +1 -1
  10. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/LICENSE +0 -0
  11. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/__init__.py +0 -0
  12. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/device_candidates.py +0 -0
  13. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/device_pool.py +0 -0
  14. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/discovery/__init__.py +0 -0
  15. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/discovery/cross_identify.py +0 -0
  16. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/discovery/lan_scan.py +0 -0
  17. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/discovery/manual_registry.py +0 -0
  18. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/discovery/usb_identity.py +0 -0
  19. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/discovery/vpn_immune.py +0 -0
  20. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/endpoint.py +0 -0
  21. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/device_discovery/protocol.py +0 -0
  22. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/mcp_plane/__init__.py +0 -0
  23. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/mcp_plane/bridge_client.py +0 -0
  24. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/mcp_plane/capability_mirror.py +0 -0
  25. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/mcp_plane/semantic_provider.py +0 -0
  26. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane/mcp_plane/token_provider.py +0 -0
  27. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane.egg-info/dependency_links.txt +0 -0
  28. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane.egg-info/entry_points.txt +0 -0
  29. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane.egg-info/requires.txt +0 -0
  30. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/debug_control_plane.egg-info/top_level.txt +0 -0
  31. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/setup.cfg +0 -0
  32. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_acceptance_flutter_app_auth.py +0 -0
  33. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_bridge_client.py +0 -0
  34. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_capability_mirror.py +0 -0
  35. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_cross_lang_kotlin_plane.py +0 -0
  36. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_device_candidates.py +0 -0
  37. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_device_pool.py +0 -0
  38. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_discovery_cross_identify.py +0 -0
  39. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_discovery_lan_scan.py +0 -0
  40. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_discovery_manual_registry.py +0 -0
  41. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_discovery_usb_identity.py +0 -0
  42. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_discovery_vpn_immune.py +0 -0
  43. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_e2e_mock.py +0 -0
  44. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_endpoint.py +0 -0
  45. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_import_sanity.py +0 -0
  46. {debug_control_plane-0.5.0 → debug_control_plane-0.5.2}/tests/test_token_provider.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debug-control-plane
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Multi-product reusable debug control plane (device discovery + MCP adapter).
5
5
  Author: tangxiaolu
6
6
  License: MIT
@@ -62,7 +62,7 @@ python/
62
62
  ├── README.md # this file
63
63
  ├── LICENSE # MIT
64
64
  └── debug_control_plane/
65
- ├── __init__.py # __version__ = "0.5.0"
65
+ ├── __init__.py # __version__ = "0.5.2"
66
66
  ├── device_discovery/ # USB/LAN device discovery + device pool
67
67
  │ ├── device_candidates.py
68
68
  │ ├── device_pool.py # identity-keyed pool, TTL expiry / 身份键池,TTL 过期
@@ -92,8 +92,8 @@ Points at `debug_control_plane.mcp_plane.server:main` — a bare server (no busi
92
92
 
93
93
  ## Version / 版本
94
94
 
95
- `0.5.0` — aligned with Kotlin/Dart/Flutter `0.5.0`, API unstable.
96
- `0.5.0` —— 与 Kotlin/Dart/Flutter `0.5.0` 对齐,API 不稳定。
95
+ `0.5.2` — aligned with Kotlin/Dart/Flutter `0.5.2`, API unstable.
96
+ `0.5.2` —— 与 Kotlin/Dart/Flutter `0.5.2` 对齐,API 不稳定。
97
97
 
98
98
  ## License / 许可证
99
99
 
@@ -43,7 +43,7 @@ python/
43
43
  ├── README.md # this file
44
44
  ├── LICENSE # MIT
45
45
  └── debug_control_plane/
46
- ├── __init__.py # __version__ = "0.5.0"
46
+ ├── __init__.py # __version__ = "0.5.2"
47
47
  ├── device_discovery/ # USB/LAN device discovery + device pool
48
48
  │ ├── device_candidates.py
49
49
  │ ├── device_pool.py # identity-keyed pool, TTL expiry / 身份键池,TTL 过期
@@ -73,8 +73,8 @@ Points at `debug_control_plane.mcp_plane.server:main` — a bare server (no busi
73
73
 
74
74
  ## Version / 版本
75
75
 
76
- `0.5.0` — aligned with Kotlin/Dart/Flutter `0.5.0`, API unstable.
77
- `0.5.0` —— 与 Kotlin/Dart/Flutter `0.5.0` 对齐,API 不稳定。
76
+ `0.5.2` — aligned with Kotlin/Dart/Flutter `0.5.2`, API unstable.
77
+ `0.5.2` —— 与 Kotlin/Dart/Flutter `0.5.2` 对齐,API 不稳定。
78
78
 
79
79
  ## License / 许可证
80
80
 
@@ -4,4 +4,4 @@ Reusable across products: device discovery (USB/WiFi/identity) + MCP adapter
4
4
  (debug HTTP protocol → MCP tool surface). Extracted from an internal app
5
5
  (R019/R020, S2 Python slice, R021-BF004).
6
6
  """
7
- __version__ = "0.5.0"
7
+ __version__ = "0.5.2"
@@ -257,7 +257,7 @@ class McpServer:
257
257
  pool: DevicePool,
258
258
  *,
259
259
  server_name: str = "mcp-debug-bridge",
260
- server_version: str = "0.5.0",
260
+ server_version: str = "0.5.2",
261
261
  providers: list[SemanticProvider] | None = None,
262
262
  tool_handlers: dict[str, Any] | None = None,
263
263
  ) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debug-control-plane
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Multi-product reusable debug control plane (device discovery + MCP adapter).
5
5
  Author: tangxiaolu
6
6
  License: MIT
@@ -62,7 +62,7 @@ python/
62
62
  ├── README.md # this file
63
63
  ├── LICENSE # MIT
64
64
  └── debug_control_plane/
65
- ├── __init__.py # __version__ = "0.5.0"
65
+ ├── __init__.py # __version__ = "0.5.2"
66
66
  ├── device_discovery/ # USB/LAN device discovery + device pool
67
67
  │ ├── device_candidates.py
68
68
  │ ├── device_pool.py # identity-keyed pool, TTL expiry / 身份键池,TTL 过期
@@ -92,8 +92,8 @@ Points at `debug_control_plane.mcp_plane.server:main` — a bare server (no busi
92
92
 
93
93
  ## Version / 版本
94
94
 
95
- `0.5.0` — aligned with Kotlin/Dart/Flutter `0.5.0`, API unstable.
96
- `0.5.0` —— 与 Kotlin/Dart/Flutter `0.5.0` 对齐,API 不稳定。
95
+ `0.5.2` — aligned with Kotlin/Dart/Flutter `0.5.2`, API unstable.
96
+ `0.5.2` —— 与 Kotlin/Dart/Flutter `0.5.2` 对齐,API 不稳定。
97
97
 
98
98
  ## License / 许可证
99
99
 
@@ -29,6 +29,7 @@ tests/test_acceptance_flutter_app_auth.py
29
29
  tests/test_bridge_client.py
30
30
  tests/test_capability_mirror.py
31
31
  tests/test_cross_lang_kotlin_plane.py
32
+ tests/test_dart_plane_persistence.py
32
33
  tests/test_device_candidates.py
33
34
  tests/test_device_pool.py
34
35
  tests/test_discovery_cross_identify.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "debug-control-plane"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "Multi-product reusable debug control plane (device discovery + MCP adapter)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,217 @@
1
+ """R005-BF002 — dart plane token 持久化验收入口(pytest 侧子集)。
2
+
3
+ 消费「已在运行的 example app Dart plane endpoint」(R002-BF005 双通道:
4
+ pytest ``--endpoint`` option 或 env ``ACCEPTANCE_ENDPOINT``;由
5
+ ``.dev-flow/R005/test-overrides/R005-BF002/run-integration.sh`` 驱动或
6
+ 操作者手动启动)。两者皆缺 → 每用例 ``pytest.skip(setup_required)``,
7
+ 绝不误报 pass。session 级健康探测 GET /hello 不可达 → 全部
8
+ skip(setup_required: endpoint unreachable)。
9
+
10
+ 覆盖范围(明示):
11
+ - 本文件只覆盖 I1/I4-TTL/I5 的 wire 子集 + I1 的 python 侧落盘断言。
12
+ - I2(冷重启)/ I3(损坏自愈)/ I4 的「篡改 app 沙箱文件 + simctl
13
+ terminate/relaunch」部分**仅在驱动脚本执行**(需要 simctl udid/bundle
14
+ 与 driver 重启能力,pytest 会话内不掌握这些环境句柄),对应断言在
15
+ ``.dev-flow/R005/test-overrides/R005-BF002/ios-simulator-persistence.py``
16
+ (I1-I5 全量)。
17
+ - 全量执行入口:``bash .dev-flow/R005/test-overrides/R005-BF002/run-integration.sh``。
18
+
19
+ Refs:
20
+ - tasks: .dev-flow/R005/dart-plane-token-persistence-tasks.md (R005-BF002)
21
+ - pattern: python/tests/test_acceptance_flutter_app_auth.py (R002-BF005)
22
+ - wire source: flutter_debug_control_plane/example/lib/src/acceptance_plane.dart
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import os
29
+ import stat
30
+ import tempfile
31
+ import time
32
+ import urllib.error
33
+ import urllib.request
34
+ import uuid
35
+ from datetime import datetime, timezone
36
+ from pathlib import Path
37
+ from typing import Any
38
+
39
+ import pytest
40
+
41
+ _ENDPOINT_ENV = "ACCEPTANCE_ENDPOINT"
42
+ _REQUEST_TIMEOUT = 5.0
43
+ _APPROVE_WAIT_TIMEOUT = 30.0
44
+ _POLL_INTERVAL = 0.5
45
+
46
+
47
+ def _get_pytest_endpoint(request: pytest.FixtureRequest) -> str | None:
48
+ """--endpoint option 优先,其次 ACCEPTANCE_ENDPOINT,皆缺返回 None。"""
49
+ option_value = request.config.getoption("--endpoint")
50
+ return option_value or os.environ.get(_ENDPOINT_ENV) or None
51
+
52
+
53
+ def _http_json(endpoint: str, method: str, path: str,
54
+ body: dict[str, Any] | None = None, bearer: str | None = None,
55
+ timeout: float = _REQUEST_TIMEOUT) -> tuple[int, Any]:
56
+ url = endpoint.rstrip("/") + path
57
+ data = json.dumps(body or {}).encode()
58
+ req = urllib.request.Request(url, data=data, method=method)
59
+ req.add_header("Content-Type", "application/json")
60
+ if bearer:
61
+ req.add_header("Authorization", f"Bearer {bearer}")
62
+ try:
63
+ with urllib.request.urlopen(req, timeout=timeout) as r:
64
+ raw = r.read()
65
+ return r.status, (json.loads(raw) if raw else {})
66
+ except urllib.error.HTTPError as e:
67
+ raw = e.read()
68
+ try:
69
+ return e.code, (json.loads(raw) if raw else {})
70
+ except ValueError:
71
+ return e.code, raw.decode(errors="replace")
72
+
73
+
74
+ class _EndpointUnreachable(Exception):
75
+ def __init__(self, reason: str) -> None:
76
+ super().__init__(reason)
77
+ self.reason = reason
78
+
79
+
80
+ @pytest.fixture(scope="session")
81
+ def plane_endpoint(request: pytest.FixtureRequest) -> str:
82
+ """endpoint 解析 + session 级健康探测;缺/不可达 → skip(setup_required)。
83
+
84
+ 与 test_acceptance_flutter_app_auth.py 同模式:session fixture 内直接
85
+ pytest.skip,依赖用例呈现 skip 而非 error。
86
+ """
87
+ endpoint = _get_pytest_endpoint(request)
88
+ if not endpoint:
89
+ pytest.skip("setup_required: no endpoint (use --endpoint or ACCEPTANCE_ENDPOINT)")
90
+ try:
91
+ st, _ = _http_json(endpoint, "GET", "/hello", timeout=3.0)
92
+ if st != 200:
93
+ pytest.skip(f"setup_required: endpoint unreachable (/hello -> {st})")
94
+ except Exception as e: # noqa: BLE001
95
+ pytest.skip(f"setup_required: endpoint unreachable ({type(e).__name__}: {e})")
96
+ return endpoint
97
+
98
+
99
+ @pytest.fixture
100
+ def endpoint_or_skip(plane_endpoint: str) -> str:
101
+ """session 探测通过的 endpoint(失败已在 session fixture skip)。"""
102
+ return plane_endpoint
103
+
104
+
105
+ def _wait_approved(endpoint: str, rid: str, nonce: str,
106
+ timeout: float = _APPROVE_WAIT_TIMEOUT) -> None:
107
+ """轮询等 driver(按 clientLabel 非 deny*/hold* 自动 approve)。"""
108
+ deadline = time.monotonic() + timeout
109
+ while time.monotonic() < deadline:
110
+ st, body = _http_json(endpoint, "POST", "/auth/status",
111
+ {"requestId": rid, "clientNonce": nonce})
112
+ if st == 200 and isinstance(body, dict) and body.get("status") == "approved":
113
+ return
114
+ if st == 403:
115
+ raise AssertionError(f"denied: {body}")
116
+ time.sleep(_POLL_INTERVAL)
117
+ raise AssertionError(f"approve timeout for {rid}")
118
+
119
+
120
+ def _auth_chain(endpoint: str, store: Path,
121
+ label: str) -> tuple[str, dict[str, Any]]:
122
+ """授权链(标准库 urllib 版,与 R002 模式一致)。"""
123
+ nonce = f"r005-py-{label}-{uuid.uuid4().hex[:8]}"
124
+ st, body = _http_json(endpoint, "POST", "/auth/request",
125
+ {"clientLabel": f"r005-py-{label}", "clientNonce": nonce})
126
+ assert st in (200, 202), f"/auth/request: {st} {body!r:.60}"
127
+ rid = body["requestId"]
128
+ _wait_approved(endpoint, rid, nonce)
129
+ st, body = _http_json(endpoint, "POST", "/auth/claim",
130
+ {"requestId": rid, "clientNonce": nonce})
131
+ assert st == 200 and isinstance(body.get("token"), str), \
132
+ f"/auth/claim: {st} {body!r:.60}"
133
+ # python 侧 FileTokenProvider 落盘(等价 R004 S1 语义)
134
+ row = {"token": body["token"], "tokenId": body.get("tokenId"),
135
+ "expiresAt": body.get("expiresAt"), "obtainedAt": datetime.now(timezone.utc).isoformat()}
136
+ store.parent.mkdir(parents=True, exist_ok=True)
137
+ store.write_text(json.dumps({"version": 1, "tokens": {"ios-sim": row}},
138
+ indent=2), encoding="utf-8")
139
+ os.chmod(store, 0o600)
140
+ return body["token"], body
141
+
142
+
143
+ @pytest.fixture
144
+ def claimed(endpoint_or_skip: str, tmp_path: Path) -> tuple[str, dict[str, Any]]:
145
+ """session 内每用例独立授权链(新 nonce 新 token)。"""
146
+ return _auth_chain(endpoint_or_skip, tmp_path / "tokens.json", "case")
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # 用例(I1/I4-TTL/I5 子集;I2/I3 仅驱动脚本执行,见模块 docstring)
151
+ # ---------------------------------------------------------------------------
152
+
153
+ def test_i1_first_auth_python_side(claimed: tuple[str, dict[str, Any]],
154
+ tmp_path: Path) -> None:
155
+ """I1 子集(python 侧):claim 200 + token 落盘 0600 + expiresAt 在场。
156
+
157
+ I1 的 app 沙箱断言(Documents/debug_control_plane/debug_auth_tokens.json
158
+ 存在/version==1/字节无明文)仅在驱动脚本执行(需 simctl 沙箱句柄)。
159
+ """
160
+ token, claim = claimed
161
+ assert token, "claim token 空"
162
+ store = tmp_path / "tokens.json"
163
+ data = json.loads(store.read_text(encoding="utf-8"))
164
+ row = data["tokens"]["ios-sim"]
165
+ assert row["token"] == token, "落盘 token 与 claim 不一致"
166
+ mode = stat.S_IMODE(store.stat().st_mode)
167
+ assert mode == 0o600, f"权限 {oct(mode)} != 0600"
168
+ assert row["tokenId"] and row["expiresAt"], f"metadata 未并入: {row!r}"
169
+
170
+
171
+ def test_i4_ttl_window(claimed: tuple[str, dict[str, Any]]) -> None:
172
+ """I4 子集(TTL 窗口):claim expiresAt - now ∈ [604790, 604810]s。
173
+
174
+ I4 的「篡改 app 沙箱 + 冷重启 → 401」部分仅在驱动脚本执行。
175
+ """
176
+ token, claim = claimed
177
+ exp = datetime.fromisoformat(claim["expiresAt"].replace("Z", "+00:00"))
178
+ delta = (exp - datetime.now(timezone.utc)).total_seconds()
179
+ assert 604790 <= delta <= 604810, f"expiresAt-now={delta:.0f}s 超出窗口"
180
+
181
+
182
+ def test_i5_no_token_401(endpoint_or_skip: str) -> None:
183
+ """I5:无 Authorization → 敏感路由 401。"""
184
+ st, _ = _http_json(endpoint_or_skip, "POST", "/debug/secure-action",
185
+ body={"via": "r005-py"})
186
+ assert st == 401, f"no-token: {st}"
187
+
188
+
189
+ def test_i5_forged_401_invalid_token(endpoint_or_skip: str) -> None:
190
+ """I5:伪造 Bearer → 401 invalid_token。"""
191
+ st, body = _http_json(endpoint_or_skip, "POST", "/debug/secure-action",
192
+ body={"via": "r005-py"}, bearer="tok-forged-r005")
193
+ assert st == 401 and body.get("code") == "invalid_token", \
194
+ f"forged: {st} {body!r:.60}"
195
+
196
+
197
+ def test_i5_pending_claim_no_token_leak(endpoint_or_skip: str) -> None:
198
+ """I5:hold* 前缀保持 pending → claim 200 status=pending 且无 token。"""
199
+ nonce = f"r005-py-hold-{uuid.uuid4().hex[:8]}"
200
+ st, body = _http_json(endpoint_or_skip, "POST", "/auth/request",
201
+ {"clientLabel": "hold-r005-py", "clientNonce": nonce})
202
+ rid = body.get("requestId")
203
+ assert st in (200, 202) and rid, f"/auth/request: {st} {body!r:.60}"
204
+ st, body = _http_json(endpoint_or_skip, "POST", "/auth/claim",
205
+ {"requestId": rid, "clientNonce": nonce})
206
+ assert st == 200 and body.get("status") == "pending" and "token" not in body, \
207
+ f"pending claim: {st} {body!r:.60}"
208
+
209
+
210
+ def test_i5_bearer_multi_capability(endpoint_or_skip: str,
211
+ claimed: tuple[str, dict[str, Any]]) -> None:
212
+ """I5:真 token Bearer 走 /debug/echo(capability 语义回归)。"""
213
+ token, _ = claimed
214
+ st, body = _http_json(endpoint_or_skip, "POST", "/debug/echo",
215
+ body={"r005": "multi"}, bearer=token)
216
+ assert st == 200 and body.get("capability") == "debug.echo", \
217
+ f"echo: {st} {body!r:.60}"
@@ -1313,7 +1313,7 @@ class TestMakeApp:
1313
1313
  srv = assembled[0]
1314
1314
  app = srv._make_app() # noqa: SLF001
1315
1315
  assert app.name == "mcp-debug-bridge"
1316
- assert app.version == "0.5.0"
1316
+ assert app.version == "0.5.2"
1317
1317
 
1318
1318
  def test_make_app_registers_stub_provider(self, assembled):
1319
1319
  """★ BF008-010 (A 类): assembled fixture 注入的 _StubProvider 已注册到