debug-control-plane 0.4.0__tar.gz → 0.5.1__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.
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/PKG-INFO +4 -4
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/README.md +3 -3
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/__init__.py +1 -1
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/mcp_plane/server.py +5 -2
- debug_control_plane-0.5.1/debug_control_plane/mcp_plane/token_provider.py +138 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/PKG-INFO +4 -4
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/SOURCES.txt +4 -1
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/pyproject.toml +1 -1
- debug_control_plane-0.5.1/tests/test_dart_plane_persistence.py +217 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_server.py +1 -1
- debug_control_plane-0.5.1/tests/test_token_provider.py +168 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/LICENSE +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/__init__.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/device_candidates.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/device_pool.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/discovery/__init__.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/discovery/cross_identify.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/discovery/lan_scan.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/discovery/manual_registry.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/discovery/usb_identity.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/discovery/vpn_immune.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/endpoint.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/device_discovery/protocol.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/mcp_plane/__init__.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/mcp_plane/bridge_client.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/mcp_plane/capability_mirror.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/mcp_plane/semantic_provider.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/dependency_links.txt +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/entry_points.txt +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/requires.txt +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/top_level.txt +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/setup.cfg +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_acceptance_flutter_app_auth.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_bridge_client.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_capability_mirror.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_cross_lang_kotlin_plane.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_device_candidates.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_device_pool.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_discovery_cross_identify.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_discovery_lan_scan.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_discovery_manual_registry.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_discovery_usb_identity.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_discovery_vpn_immune.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_e2e_mock.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_endpoint.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_import_sanity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debug-control-plane
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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.
|
|
65
|
+
├── __init__.py # __version__ = "0.5.1"
|
|
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.
|
|
96
|
-
`0.
|
|
95
|
+
`0.5.1` — aligned with Kotlin/Dart/Flutter `0.5.1`, API unstable.
|
|
96
|
+
`0.5.1` —— 与 Kotlin/Dart/Flutter `0.5.1` 对齐,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.
|
|
46
|
+
├── __init__.py # __version__ = "0.5.1"
|
|
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.
|
|
77
|
-
`0.
|
|
76
|
+
`0.5.1` — aligned with Kotlin/Dart/Flutter `0.5.1`, API unstable.
|
|
77
|
+
`0.5.1` —— 与 Kotlin/Dart/Flutter `0.5.1` 对齐,API 不稳定。
|
|
78
78
|
|
|
79
79
|
## License / 许可证
|
|
80
80
|
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/mcp_plane/server.py
RENAMED
|
@@ -87,6 +87,7 @@ from .bridge_client import (
|
|
|
87
87
|
)
|
|
88
88
|
from .capability_mirror import CapabilityMirror, CapabilitySchema, ToolSpec
|
|
89
89
|
from .semantic_provider import SemanticProvider
|
|
90
|
+
from .token_provider import FileTokenProvider
|
|
90
91
|
|
|
91
92
|
# ★ BF008-010 (Contract §0.1 边界 1 收尾 + 方案 X): capability-specific semantic
|
|
92
93
|
# sugar 已迁业务侧(强耦合产品 protocol,属业务知识)。平面 server 零业务依赖
|
|
@@ -256,7 +257,7 @@ class McpServer:
|
|
|
256
257
|
pool: DevicePool,
|
|
257
258
|
*,
|
|
258
259
|
server_name: str = "mcp-debug-bridge",
|
|
259
|
-
server_version: str = "0.
|
|
260
|
+
server_version: str = "0.5.1",
|
|
260
261
|
providers: list[SemanticProvider] | None = None,
|
|
261
262
|
tool_handlers: dict[str, Any] | None = None,
|
|
262
263
|
) -> None:
|
|
@@ -1052,7 +1053,9 @@ def main() -> None:
|
|
|
1052
1053
|
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
1053
1054
|
)
|
|
1054
1055
|
pool = DevicePool(persist_path=Path.home() / ".debug-control-plane" / "devices.json")
|
|
1055
|
-
client = BridgeClient(
|
|
1056
|
+
client = BridgeClient(
|
|
1057
|
+
pool=pool, token_provider=FileTokenProvider()
|
|
1058
|
+
)
|
|
1056
1059
|
mirror = CapabilityMirror(client=client)
|
|
1057
1060
|
server = McpServer(mirror=mirror, client=client, pool=pool)
|
|
1058
1061
|
server.run_stdio() # blocks; exits when the AI client closes stdin
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""File-backed debug auth token provider (R004-BF001).
|
|
2
|
+
|
|
3
|
+
FileTokenProvider is the production implementation of
|
|
4
|
+
:class:`debug_control_plane.mcp_plane.bridge_client.DebugAuthTokenProvider`.
|
|
5
|
+
It persists per-device bearer tokens to ``~/.debug-control-plane/tokens.json``
|
|
6
|
+
so tokens survive python process restarts (design R004 §3.2).
|
|
7
|
+
|
|
8
|
+
Schema (independent from devices.json — no shared structure or imports)::
|
|
9
|
+
|
|
10
|
+
{"version": 1, "tokens": {device_id: {"token": ..., "tokenId": ...,
|
|
11
|
+
"expiresAt": ...}}}
|
|
12
|
+
|
|
13
|
+
Concurrency model: lazy-loaded, single-threaded. This provider assumes it is
|
|
14
|
+
only used from the asyncio event-loop thread (the MCP server's threading
|
|
15
|
+
model). No locking is performed — the design explicitly accepted this
|
|
16
|
+
asyncio-single-thread assumption.
|
|
17
|
+
|
|
18
|
+
Security: plaintext tokens on the developer machine with 0600 permissions
|
|
19
|
+
(user-approved decision). The 0600 mode is guaranteed by creating the tmp
|
|
20
|
+
file via ``os.open(..., 0o600)`` (bypassing umask, so no 0644 window exists)
|
|
21
|
+
followed by an atomic ``os.replace``.
|
|
22
|
+
|
|
23
|
+
Corruption policy: a corrupt file or an unknown ``version`` silently falls
|
|
24
|
+
back to an empty store — tokens are regenerable and the server must not
|
|
25
|
+
crash on load.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Mapping
|
|
35
|
+
|
|
36
|
+
_SCHEMA_VERSION = 1
|
|
37
|
+
_DEFAULT_PATH = Path.home() / ".debug-control-plane" / "tokens.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_expired(value: str) -> bool:
|
|
41
|
+
"""Return True if ``value`` is a tz-aware ISO timestamp in the past.
|
|
42
|
+
|
|
43
|
+
Parse failures and naive timestamps are treated as *not* expired: the
|
|
44
|
+
phone's 401 is the final arbiter, and a parsing bug must not break the
|
|
45
|
+
auth chain (design §5). Evaluated at read time; never written back.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
49
|
+
except (ValueError, TypeError):
|
|
50
|
+
return False
|
|
51
|
+
if parsed.tzinfo is None:
|
|
52
|
+
return False
|
|
53
|
+
return parsed <= datetime.now(timezone.utc)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FileTokenProvider:
|
|
57
|
+
"""File-backed token store implementing the DebugAuthTokenProvider protocol."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, path: Path | None = None) -> None:
|
|
60
|
+
self._path = path or _DEFAULT_PATH
|
|
61
|
+
self._loaded = False
|
|
62
|
+
self._tokens: dict[str, dict[str, str]] = {}
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# Protocol surface
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def get_token(self, device_id: str) -> str | None:
|
|
69
|
+
"""Return the stored token for ``device_id`` if present and unexpired."""
|
|
70
|
+
self._ensure_loaded()
|
|
71
|
+
row = self._tokens.get(device_id)
|
|
72
|
+
if row is None:
|
|
73
|
+
return None
|
|
74
|
+
expires_at = row.get("expiresAt", "")
|
|
75
|
+
if expires_at and _is_expired(expires_at):
|
|
76
|
+
return None
|
|
77
|
+
return row.get("token")
|
|
78
|
+
|
|
79
|
+
def save_token(
|
|
80
|
+
self, device_id: str, token: str, metadata: Mapping[str, Any]
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Store ``token`` for ``device_id`` and persist (full file rewrite)."""
|
|
83
|
+
self._ensure_loaded()
|
|
84
|
+
row: dict[str, str] = {"token": token}
|
|
85
|
+
for key, value in metadata.items():
|
|
86
|
+
row[key] = value if isinstance(value, str) else str(value)
|
|
87
|
+
self._tokens[device_id] = row
|
|
88
|
+
self._flush()
|
|
89
|
+
|
|
90
|
+
def clear_token(self, device_id: str, reason: str) -> None:
|
|
91
|
+
"""Drop the row for ``device_id`` and persist (rewrite without it)."""
|
|
92
|
+
self._ensure_loaded()
|
|
93
|
+
if device_id in self._tokens:
|
|
94
|
+
del self._tokens[device_id]
|
|
95
|
+
self._flush()
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------
|
|
98
|
+
# Internals
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def _ensure_loaded(self) -> None:
|
|
102
|
+
if self._loaded:
|
|
103
|
+
return
|
|
104
|
+
self._loaded = True
|
|
105
|
+
try:
|
|
106
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
107
|
+
except (OSError, ValueError):
|
|
108
|
+
return # missing or corrupt → silent empty fallback
|
|
109
|
+
if not isinstance(data, dict) or data.get("version") != _SCHEMA_VERSION:
|
|
110
|
+
return # unknown schema → silent empty fallback
|
|
111
|
+
tokens = data.get("tokens")
|
|
112
|
+
if not isinstance(tokens, dict):
|
|
113
|
+
return
|
|
114
|
+
self._tokens = {
|
|
115
|
+
device_id: row
|
|
116
|
+
for device_id, row in tokens.items()
|
|
117
|
+
if isinstance(row, dict)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def _flush(self) -> None:
|
|
121
|
+
"""Atomically write the store with 0600 permissions."""
|
|
122
|
+
payload = json.dumps(
|
|
123
|
+
{"version": _SCHEMA_VERSION, "tokens": self._tokens},
|
|
124
|
+
indent=2,
|
|
125
|
+
ensure_ascii=False,
|
|
126
|
+
)
|
|
127
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
tmp_path = self._path.with_name(self._path.name + ".tmp")
|
|
129
|
+
fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
130
|
+
try:
|
|
131
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
132
|
+
f.write(payload)
|
|
133
|
+
f.flush()
|
|
134
|
+
os.fsync(f.fileno())
|
|
135
|
+
except BaseException:
|
|
136
|
+
tmp_path.unlink(missing_ok=True)
|
|
137
|
+
raise
|
|
138
|
+
os.replace(tmp_path, self._path)
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debug-control-plane
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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.
|
|
65
|
+
├── __init__.py # __version__ = "0.5.1"
|
|
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.
|
|
96
|
-
`0.
|
|
95
|
+
`0.5.1` — aligned with Kotlin/Dart/Flutter `0.5.1`, API unstable.
|
|
96
|
+
`0.5.1` —— 与 Kotlin/Dart/Flutter `0.5.1` 对齐,API 不稳定。
|
|
97
97
|
|
|
98
98
|
## License / 许可证
|
|
99
99
|
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/SOURCES.txt
RENAMED
|
@@ -24,10 +24,12 @@ debug_control_plane/mcp_plane/bridge_client.py
|
|
|
24
24
|
debug_control_plane/mcp_plane/capability_mirror.py
|
|
25
25
|
debug_control_plane/mcp_plane/semantic_provider.py
|
|
26
26
|
debug_control_plane/mcp_plane/server.py
|
|
27
|
+
debug_control_plane/mcp_plane/token_provider.py
|
|
27
28
|
tests/test_acceptance_flutter_app_auth.py
|
|
28
29
|
tests/test_bridge_client.py
|
|
29
30
|
tests/test_capability_mirror.py
|
|
30
31
|
tests/test_cross_lang_kotlin_plane.py
|
|
32
|
+
tests/test_dart_plane_persistence.py
|
|
31
33
|
tests/test_device_candidates.py
|
|
32
34
|
tests/test_device_pool.py
|
|
33
35
|
tests/test_discovery_cross_identify.py
|
|
@@ -38,4 +40,5 @@ tests/test_discovery_vpn_immune.py
|
|
|
38
40
|
tests/test_e2e_mock.py
|
|
39
41
|
tests/test_endpoint.py
|
|
40
42
|
tests/test_import_sanity.py
|
|
41
|
-
tests/test_server.py
|
|
43
|
+
tests/test_server.py
|
|
44
|
+
tests/test_token_provider.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "debug-control-plane"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.1"
|
|
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.
|
|
1316
|
+
assert app.version == "0.5.1"
|
|
1317
1317
|
|
|
1318
1318
|
def test_make_app_registers_stub_provider(self, assembled):
|
|
1319
1319
|
"""★ BF008-010 (A 类): assembled fixture 注入的 _StubProvider 已注册到
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Unit tests for FileTokenProvider (R004-BF001).
|
|
2
|
+
|
|
3
|
+
Covers the 7 BF001-T scenarios from test §2:
|
|
4
|
+
1. roundtrip-cross-instance: save → new instance get hits disk.
|
|
5
|
+
2. 0600-permission: file mode == 0o600 after save.
|
|
6
|
+
3. expired-past-future-Z-invalid: expiresAt four-state judgement.
|
|
7
|
+
4. clear-deletes-row: clear_token rewrites file without the row.
|
|
8
|
+
5. corrupt-fallback: invalid JSON → silent empty fallback.
|
|
9
|
+
6. metadata-merge: metadata str values merged into the row.
|
|
10
|
+
7. version-mismatch: version != 1 → silent empty fallback.
|
|
11
|
+
|
|
12
|
+
All tests use tmp_path — the real ~/.debug-control-plane/ is never touched.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import stat
|
|
19
|
+
from datetime import datetime, timedelta, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from debug_control_plane.mcp_plane.token_provider import FileTokenProvider
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _provider(tmp_path: Path) -> FileTokenProvider:
|
|
26
|
+
return FileTokenProvider(path=tmp_path / "tokens.json")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _write_raw(tmp_path: Path, payload: dict) -> Path:
|
|
30
|
+
p = tmp_path / "tokens.json"
|
|
31
|
+
p.write_text(json.dumps(payload), encoding="utf-8")
|
|
32
|
+
return p
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# 1. roundtrip-cross-instance
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def test_roundtrip_cross_instance(tmp_path: Path) -> None:
|
|
40
|
+
p1 = _provider(tmp_path)
|
|
41
|
+
p1.save_token(
|
|
42
|
+
"device-1",
|
|
43
|
+
"dcp_tok_1",
|
|
44
|
+
{"tokenId": "tid-1", "expiresAt": "2099-01-01T00:00:00+00:00"},
|
|
45
|
+
)
|
|
46
|
+
# A new instance must see the token (persistence, not memory).
|
|
47
|
+
p2 = _provider(tmp_path)
|
|
48
|
+
assert p2.get_token("device-1") == "dcp_tok_1"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# 2. 0600-permission
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def test_file_mode_0600(tmp_path: Path) -> None:
|
|
56
|
+
provider = _provider(tmp_path)
|
|
57
|
+
provider.save_token("device-1", "dcp_tok_1", {"tokenId": "tid-1"})
|
|
58
|
+
mode = stat.S_IMODE(provider._path.stat().st_mode) # noqa: SLF001
|
|
59
|
+
assert mode == 0o600
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# 3. expired four states
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def test_expired_past_returns_none(tmp_path: Path) -> None:
|
|
67
|
+
past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
|
|
68
|
+
provider = _provider(tmp_path)
|
|
69
|
+
provider.save_token("d", "t", {"expiresAt": past})
|
|
70
|
+
assert provider.get_token("d") is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_not_expired_future(tmp_path: Path) -> None:
|
|
74
|
+
future = (datetime.now(timezone.utc) + timedelta(days=6)).isoformat()
|
|
75
|
+
provider = _provider(tmp_path)
|
|
76
|
+
provider.save_token("d", "t", {"expiresAt": future})
|
|
77
|
+
assert provider.get_token("d") == "t"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_not_expired_z_suffix(tmp_path: Path) -> None:
|
|
81
|
+
future = (
|
|
82
|
+
datetime.now(timezone.utc) + timedelta(days=6)
|
|
83
|
+
).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
84
|
+
provider = _provider(tmp_path)
|
|
85
|
+
provider.save_token("d", "t", {"expiresAt": future})
|
|
86
|
+
assert provider.get_token("d") == "t"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_invalid_expiresat_treated_as_not_expired(tmp_path: Path) -> None:
|
|
90
|
+
provider = _provider(tmp_path)
|
|
91
|
+
provider.save_token("d", "t", {"expiresAt": "not-a-date"})
|
|
92
|
+
assert provider.get_token("d") == "t"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_naive_expiresat_treated_as_not_expired(tmp_path: Path) -> None:
|
|
96
|
+
provider = _provider(tmp_path)
|
|
97
|
+
provider.save_token("d", "t", {"expiresAt": "2099-01-01T00:00:00"})
|
|
98
|
+
assert provider.get_token("d") == "t"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# 4. clear-deletes-row
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def test_clear_deletes_row_on_disk(tmp_path: Path) -> None:
|
|
106
|
+
provider = _provider(tmp_path)
|
|
107
|
+
provider.save_token("d1", "t1", {"tokenId": "tid-1"})
|
|
108
|
+
provider.save_token("d2", "t2", {"tokenId": "tid-2"})
|
|
109
|
+
provider.clear_token("d1", reason="token_expired")
|
|
110
|
+
assert provider.get_token("d1") is None
|
|
111
|
+
assert provider.get_token("d2") == "t2"
|
|
112
|
+
# A fresh instance sees the same on-disk state.
|
|
113
|
+
assert _provider(tmp_path).get_token("d1") is None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_clear_missing_device_is_noop(tmp_path: Path) -> None:
|
|
117
|
+
provider = _provider(tmp_path)
|
|
118
|
+
provider.clear_token("ghost", reason="token_expired") # must not raise
|
|
119
|
+
assert provider.get_token("ghost") is None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# 5. corrupt-fallback
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def test_corrupt_file_silent_fallback(tmp_path: Path) -> None:
|
|
127
|
+
p = tmp_path / "tokens.json"
|
|
128
|
+
p.write_text("{not valid json", encoding="utf-8")
|
|
129
|
+
provider = FileTokenProvider(path=p)
|
|
130
|
+
assert provider.get_token("d") is None # no raise, empty
|
|
131
|
+
# After a save the file becomes valid again (self-heal).
|
|
132
|
+
provider.save_token("d", "t", {"tokenId": "tid"})
|
|
133
|
+
assert json.loads(p.read_text(encoding="utf-8"))["version"] == 1
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# 6. metadata-merge
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def test_metadata_merged_into_row(tmp_path: Path) -> None:
|
|
141
|
+
provider = _provider(tmp_path)
|
|
142
|
+
provider.save_token("d", "t", {"tokenId": "tid-1", "expiresAt": "x", "count": 3})
|
|
143
|
+
row = json.loads((tmp_path / "tokens.json").read_text(encoding="utf-8"))[
|
|
144
|
+
"tokens"
|
|
145
|
+
]["d"]
|
|
146
|
+
assert row["token"] == "t"
|
|
147
|
+
assert row["tokenId"] == "tid-1"
|
|
148
|
+
assert row["expiresAt"] == "x"
|
|
149
|
+
# Non-str metadata values are stringified (schema keeps str values only).
|
|
150
|
+
assert isinstance(row["count"], str)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# 7. version-mismatch
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def test_version_mismatch_fallback(tmp_path: Path) -> None:
|
|
158
|
+
_write_raw(
|
|
159
|
+
tmp_path,
|
|
160
|
+
{"version": 2, "tokens": {"d": {"token": "t", "tokenId": "tid"}}},
|
|
161
|
+
)
|
|
162
|
+
provider = _provider(tmp_path)
|
|
163
|
+
assert provider.get_token("d") is None
|
|
164
|
+
# Save under v1 schema after the fallback.
|
|
165
|
+
provider.save_token("d", "t2", {})
|
|
166
|
+
data = json.loads((tmp_path / "tokens.json").read_text(encoding="utf-8"))
|
|
167
|
+
assert data["version"] == 1
|
|
168
|
+
assert data["tokens"]["d"]["token"] == "t2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane/mcp_plane/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/requires.txt
RENAMED
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/debug_control_plane.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_acceptance_flutter_app_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_cross_lang_kotlin_plane.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_discovery_cross_identify.py
RENAMED
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_discovery_manual_registry.py
RENAMED
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.1}/tests/test_discovery_usb_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|