From 00040e5ce48aaeec5872237423c68a0a0e170b9c Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 26 Jul 2026 18:06:43 +0300 Subject: [PATCH] Preserve extension names in module preflight --- docs/runbooks/adapter-1c-mcp.md | 20 +-- plugins/1c/connector/adapter_1c_server.py | 35 ++++- scripts/check_1c_verify_reports.py | 103 +++++++++++++- scripts/smoke_1c_write_preflight.py | 166 ++++++++++++++++++++++ scripts/verify_1c_adapter_deployment.ps1 | 4 +- tests/1c/test_payload_codec.py | 63 +++++++- tests/1c/test_write_preflight_smoke.py | 66 +++++++++ 7 files changed, 440 insertions(+), 17 deletions(-) diff --git a/docs/runbooks/adapter-1c-mcp.md b/docs/runbooks/adapter-1c-mcp.md index f07eaef..46d19dd 100644 --- a/docs/runbooks/adapter-1c-mcp.md +++ b/docs/runbooks/adapter-1c-mcp.md @@ -777,14 +777,18 @@ which save-layer table is used by the copy plan and saved-state write smokes. Add `-RequireSelectorChainWritePlanComposition` when the selected base/object must have a saved-state stream that lets the selector-chain smoke compose a concrete read-only `metadata.write.plan`. The same strict mode also requires -the write-preflight smoke to discover an extension form by public -`extension/ref/form/member` names, compose an allowed plan, and prove that the -repository and support gates use the same resolved `extension:` layer. -It then submits the same public selector with a deliberately different GUID -and requires a read-only `blocked / extension_selector_conflict` result, so a -legacy permissive repository profile cannot authorize a mismatched layer. -Run that check directly with -`scripts/smoke_1c_write_preflight.py --require-name-first-extension-form`. +the write-preflight smoke to discover both an extension form and an extension +module by public names (`extension/ref/form/member` and +`extension/ref/form/module/stream_ordinal`), compose allowed plans, and prove +that the repository and support gates use the same resolved +`extension:` layer. Module search restores the public extension name from +`ConfigCASSave`; it never returns the storage GUID as the caller-facing +selector. The smoke then submits each public selector with a deliberately +different GUID and requires a read-only +`blocked / extension_selector_conflict` result, so a legacy permissive +repository profile cannot authorize a mismatched layer. Run both checks +directly with +`scripts/smoke_1c_write_preflight.py --require-name-first-extension-form --require-name-first-extension-module`. To exercise the MCP proxy itself, switch transport and URL: diff --git a/plugins/1c/connector/adapter_1c_server.py b/plugins/1c/connector/adapter_1c_server.py index 408e0fc..e5a63fe 100644 --- a/plugins/1c/connector/adapter_1c_server.py +++ b/plugins/1c/connector/adapter_1c_server.py @@ -41912,7 +41912,11 @@ def metadata_saved_state_modules_owner_guid_from_selector( } -def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any] | None: +def public_saved_state_modules_search_row( + row: dict[str, Any], + *, + requested_extension: str | None = None, +) -> dict[str, Any] | None: owner = row.get("owner") if isinstance(row.get("owner"), dict) else {} form = row.get("form") if isinstance(row.get("form"), dict) else {} module = row.get("module") if isinstance(row.get("module"), dict) else {} @@ -41936,6 +41940,7 @@ def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any] } qualified_name = str(row.get("qualified_name") or row.get("display_name") or "").strip() selector = { + **({"extension": requested_extension} if requested_extension else {}), **({"ref": owner_ref} if owner_ref else {}), **({"form": form_public.get("name")} if form_public.get("name") else {}), **({"module": module_public.get("name")} if module_public.get("name") else {}), @@ -41960,6 +41965,7 @@ def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any] stream_qualified_name = str(stream.get("qualified_name") or stream.get("display_name") or qualified_name).strip() write_plan_target = { "kind": "module", + **({"extension": requested_extension} if requested_extension else {}), **({"ref": stream_owner_ref} if stream_owner_ref else {}), **({"form": stream_form.get("name")} if stream_form.get("name") else {}), **({"module": stream_module.get("name")} if stream_module.get("name") else {}), @@ -42173,11 +42179,28 @@ def metadata_saved_state_modules_search(payload: dict[str, Any]) -> dict[str, An break if len(modules) >= int(limit or 50): break - public_modules = modules if include_storage else [ - public_row - for row in modules - if (public_row := public_saved_state_modules_search_row(row)) is not None - ] + public_modules = modules if include_storage else [] + if not include_storage: + extension_names_by_guid: dict[str, str] = {} + if not extension_filter and any(row.get("table") == "ConfigCASSave" for row in modules): + extension_names_by_guid = { + str(guid).strip().lower(): str(item.get("name") or "").strip() + for guid, item in extension_map_by_guid(base_id).items() + if str(item.get("name") or "").strip() + } + for row in modules: + row_extension = extension_filter or None + if not row_extension and row.get("table") == "ConfigCASSave": + row_file_name = str(row.get("file_name") or "") + extension_guid_from_file = row_file_name.split("__", 1)[0].strip().lower() if "__" in row_file_name else "" + if extension_guid_from_file: + row_extension = extension_names_by_guid.get(extension_guid_from_file) + public_row = public_saved_state_modules_search_row( + row, + requested_extension=row_extension, + ) + if public_row is not None: + public_modules.append(public_row) result = { "schema": "onec_saved_state_module_search.v1", "status": "ok", diff --git a/scripts/check_1c_verify_reports.py b/scripts/check_1c_verify_reports.py index 59f3f76..f828cd6 100644 --- a/scripts/check_1c_verify_reports.py +++ b/scripts/check_1c_verify_reports.py @@ -401,7 +401,15 @@ def validate_write_preflight( failures.append({"code": "write_preflight_not_ok", "label": label, "path": str(path), "status": report.get("status")}) if report.get("failures"): failures.append({"code": "write_preflight_failures_present", "label": label, "path": str(path), "failures": report.get("failures")}) - for check in ("method_exposed", "effective_path_preflight", "concrete_saved_state_preflight", "name_first_extension_form_preflight", "conflicting_extension_selector_preflight"): + for check in ( + "method_exposed", + "effective_path_preflight", + "concrete_saved_state_preflight", + "name_first_extension_form_preflight", + "conflicting_extension_selector_preflight", + "name_first_extension_module_preflight", + "conflicting_extension_module_selector_preflight", + ): if check not in checks: failures.append({"code": "write_preflight_check_missing", "label": label, "check": check, "path": str(path)}) expect_check(checks, failures, label, path, "method_exposed", {"status": "ok"}, failure_code="write_preflight_check_field_unexpected") @@ -495,6 +503,80 @@ def validate_write_preflight( }, failure_code="write_preflight_check_field_unexpected", ) + name_first_module = checks.get("name_first_extension_module_preflight") if isinstance(checks.get("name_first_extension_module_preflight"), dict) else {} + require_name_first_module = bool( + (report.get("requirements") or {}).get("name_first_extension_module") + if isinstance(report.get("requirements"), dict) + else False + ) + if name_first_module.get("status") == "skipped_no_public_extension_module_target": + if require_name_first_module: + failures.append({ + "code": "write_preflight_name_first_extension_module_required", + "label": label, + "path": str(path), + }) + else: + expect_check( + checks, + failures, + label, + path, + "name_first_extension_module_preflight", + { + "schema": "onec_metadata_write_preflight.v1", + "plan_status": "planned", + "plan_allowed": True, + "name_first": True, + }, + failure_code="write_preflight_check_field_unexpected", + ) + repository_layer = str(name_first_module.get("repository_layer_id") or "") + support_layer = str(name_first_module.get("support_layer_id") or "") + if not EXTENSION_GUID_LAYER_RE.fullmatch(repository_layer): + failures.append({ + "code": "write_preflight_extension_module_layer_unresolved", + "label": label, + "path": str(path), + "repository_layer_id": repository_layer or None, + }) + if repository_layer != support_layer: + failures.append({ + "code": "write_preflight_extension_module_layer_mismatch", + "label": label, + "path": str(path), + "repository_layer_id": repository_layer or None, + "support_layer_id": support_layer or None, + }) + conflicting_extension_module = ( + checks.get("conflicting_extension_module_selector_preflight") + if isinstance(checks.get("conflicting_extension_module_selector_preflight"), dict) + else {} + ) + if conflicting_extension_module.get("status") == "skipped_no_public_extension_module_target": + if require_name_first_module: + failures.append({ + "code": "write_preflight_extension_module_conflict_check_required", + "label": label, + "path": str(path), + }) + else: + expect_check( + checks, + failures, + label, + path, + "conflicting_extension_module_selector_preflight", + { + "schema": "onec_metadata_write_preflight.v1", + "status": "blocked", + "allowed": False, + "resolution_status": "conflict", + "error": "extension_selector_conflict", + "layer_id": "extension:unresolved", + }, + failure_code="write_preflight_check_field_unexpected", + ) if require_mcp_initialize: if "mcp.initialize" not in checks: failures.append({"code": "write_preflight_check_missing", "label": label, "check": "mcp.initialize", "path": str(path)}) @@ -1666,6 +1748,25 @@ def write_self_test_reports(report_dir: Path, *, base_id: str, composed: bool, s "error": "extension_selector_conflict", "layer_id": "extension:unresolved", }, + "name_first_extension_module_preflight": { + "schema": "onec_metadata_write_preflight.v1", + "status": "ready", + "allowed": True, + "plan_status": "planned", + "plan_allowed": True, + "repository_layer_id": "extension:11111111-1111-1111-1111-111111111111", + "support_layer_id": "extension:11111111-1111-1111-1111-111111111111", + "name_first": True, + "extension": "test2", + }, + "conflicting_extension_module_selector_preflight": { + "schema": "onec_metadata_write_preflight.v1", + "status": "blocked", + "allowed": False, + "resolution_status": "conflict", + "error": "extension_selector_conflict", + "layer_id": "extension:unresolved", + }, }, "failures": [], } diff --git a/scripts/smoke_1c_write_preflight.py b/scripts/smoke_1c_write_preflight.py index 9656aa0..7ee573d 100644 --- a/scripts/smoke_1c_write_preflight.py +++ b/scripts/smoke_1c_write_preflight.py @@ -207,6 +207,57 @@ def first_public_extension_form_candidate(result: dict[str, Any]) -> dict[str, A return candidate +def first_public_extension_module_candidate(result: dict[str, Any]) -> dict[str, Any] | None: + public_keys = { + "extension", + "ref", + "form", + "module", + "qualified_name", + "stream_ordinal", + } + opaque_keys = { + "table", + "file_name", + "guid", + "form_guid", + "module_guid", + "object_guid", + "module_ref", + "extension_guid", + } + for module in result.get("modules") or []: + if not isinstance(module, dict): + continue + for stream in module.get("streams") or []: + if not isinstance(stream, dict): + continue + target = stream.get("write_plan_target") if isinstance(stream.get("write_plan_target"), dict) else {} + preview = str(stream.get("preview") or "").strip() + control_fragment = next((line.strip() for line in preview.splitlines() if line.strip()), "") + if not target.get("extension") or not target.get("ref") or not target.get("module"): + continue + if not preview or not control_fragment or any(key in target for key in opaque_keys): + continue + public_target = { + "kind": "module", + **{ + key: value + for key, value in target.items() + if key in public_keys and value not in (None, "") + }, + } + return { + "target": public_target, + "intent": { + "operation": "replace_with_control", + "control_fragment": control_fragment, + "new": preview, + }, + } + return None + + def different_extension_guid(layer_id: str) -> str | None: if not EXTENSION_GUID_LAYER_RE.fullmatch(str(layer_id or "")): return None @@ -222,6 +273,7 @@ def run_smoke( *, transport: str, require_name_first_extension_form: bool = False, + require_name_first_extension_module: bool = False, ) -> dict[str, Any]: failures: list[str] = [] checks: dict[str, Any] = {} @@ -410,6 +462,117 @@ def run_smoke( require(layer_resolution.get("status") == "conflict", "conflicting extension selector must report conflict resolution status", failures) require(layer_diagnostics.get("error") == "extension_selector_conflict", "conflicting extension selector must report extension_selector_conflict", failures) + modules = rpc_call( + endpoint_url, + "metadata.saved_state.modules.search", + { + "base_id": base_id, + "tables": ["ConfigCASSave"], + "limit": 20, + "scan_limit": 1000, + "timeout_seconds": int(timeout), + }, + timeout, + transport=transport, + session_id=session_id, + ) + module_candidate = first_public_extension_module_candidate(modules) + if not module_candidate: + checks["name_first_extension_module_preflight"] = { + "status": "skipped_no_public_extension_module_target", + "search_status": modules.get("status"), + "modules": int((modules.get("counts") or {}).get("modules") or 0), + } + checks["conflicting_extension_module_selector_preflight"] = { + "status": "skipped_no_public_extension_module_target", + } + if require_name_first_extension_module: + failures.append("name-first extension module preflight target is required but was not discovered") + else: + module_preflight = rpc_call( + endpoint_url, + "metadata.write.preflight", + { + "base_id": base_id, + "target": module_candidate["target"], + "intent": module_candidate["intent"], + "resolve_origin": False, + "timeout_seconds": int(timeout), + }, + timeout, + transport=transport, + session_id=session_id, + ) + plan = module_preflight.get("plan") if isinstance(module_preflight.get("plan"), dict) else {} + repository = module_preflight.get("repository") if isinstance(module_preflight.get("repository"), dict) else {} + support = module_preflight.get("support") if isinstance(module_preflight.get("support"), dict) else {} + repository_layer = str(repository.get("layer_id") or "") + support_layer = str(support.get("layer_id") or "") + name_first = not any( + key in module_candidate["target"] + for key in ("table", "file_name", "guid", "form_guid", "module_guid", "object_guid", "module_ref", "extension_guid") + ) + checks["name_first_extension_module_preflight"] = { + "schema": module_preflight.get("schema"), + "status": module_preflight.get("status"), + "allowed": module_preflight.get("allowed"), + "plan_status": plan.get("status"), + "plan_allowed": plan.get("allowed"), + "repository_layer_id": repository_layer or None, + "support_layer_id": support_layer or None, + "name_first": name_first, + "extension": module_candidate["target"].get("extension"), + } + require(module_preflight.get("schema") == "onec_metadata_write_preflight.v1", "name-first extension module preflight must return expected schema", failures) + require(classified_preflight_status(module_preflight.get("status")), "name-first extension module preflight must classify readiness or a safety gate", failures) + require(plan.get("status") == "planned" and plan.get("allowed") is True, "name-first extension module preflight plan must be allowed and planned", failures) + require(name_first, "extension module preflight input must remain name-first", failures) + require(bool(EXTENSION_GUID_LAYER_RE.fullmatch(repository_layer)), "module repository gate must use a resolved extension GUID layer", failures) + require(repository_layer == support_layer, "module repository and support gates must use the same extension layer", failures) + conflicting_guid = different_extension_guid(repository_layer) + if not conflicting_guid: + checks["conflicting_extension_module_selector_preflight"] = { + "status": "skipped_no_resolved_extension_layer", + } + else: + conflicting = rpc_call( + endpoint_url, + "metadata.write.preflight", + { + "base_id": base_id, + "extension_guid": conflicting_guid, + "target": module_candidate["target"], + "intent": module_candidate["intent"], + "resolve_origin": False, + "timeout_seconds": int(timeout), + }, + timeout, + transport=transport, + session_id=session_id, + ) + layer_resolution = ( + conflicting.get("development_layer_resolution") + if isinstance(conflicting.get("development_layer_resolution"), dict) + else {} + ) + layer_diagnostics = ( + layer_resolution.get("diagnostics") + if isinstance(layer_resolution.get("diagnostics"), dict) + else {} + ) + checks["conflicting_extension_module_selector_preflight"] = { + "schema": conflicting.get("schema"), + "status": conflicting.get("status"), + "allowed": conflicting.get("allowed"), + "resolution_status": layer_resolution.get("status"), + "error": layer_diagnostics.get("error"), + "layer_id": layer_resolution.get("layer_id"), + } + require(conflicting.get("schema") == "onec_metadata_write_preflight.v1", "conflicting extension module selector preflight must return expected schema", failures) + require(conflicting.get("status") == "blocked" and conflicting.get("allowed") is False, "conflicting extension module name/GUID must block preflight", failures) + require(layer_resolution.get("status") == "conflict", "conflicting extension module selector must report conflict resolution status", failures) + require(layer_diagnostics.get("error") == "extension_selector_conflict", "conflicting extension module selector must report extension_selector_conflict", failures) + return { "schema": "onec_write_preflight_smoke.v1", "status": "ok" if not failures else "failed", @@ -418,6 +581,7 @@ def run_smoke( "base_id": base_id, "requirements": { "name_first_extension_form": require_name_first_extension_form, + "name_first_extension_module": require_name_first_extension_module, }, "checks": checks, "failures": failures, @@ -434,6 +598,7 @@ def main() -> int: parser.add_argument("--timeout", type=float, default=30.0) parser.add_argument("--report", type=Path) parser.add_argument("--require-name-first-extension-form", action="store_true") + parser.add_argument("--require-name-first-extension-module", action="store_true") args = parser.parse_args() endpoint_url = args.mcp_url if args.transport == "mcp" else args.base_url @@ -444,6 +609,7 @@ def main() -> int: args.timeout, transport=args.transport, require_name_first_extension_form=args.require_name_first_extension_form, + require_name_first_extension_module=args.require_name_first_extension_module, ) except (TimeoutError, URLError, OSError) as exc: report = { diff --git a/scripts/verify_1c_adapter_deployment.ps1 b/scripts/verify_1c_adapter_deployment.ps1 index 137c9d3..48e6242 100644 --- a/scripts/verify_1c_adapter_deployment.ps1 +++ b/scripts/verify_1c_adapter_deployment.ps1 @@ -206,7 +206,7 @@ function Assert-WritePreflightReport { if ($report.failures -and $report.failures.Count -gt 0) { throw "$Label report contains failures: $Path" } - foreach ($check in @("method_exposed", "effective_path_preflight", "concrete_saved_state_preflight", "name_first_extension_form_preflight", "conflicting_extension_selector_preflight")) { + foreach ($check in @("method_exposed", "effective_path_preflight", "concrete_saved_state_preflight", "name_first_extension_form_preflight", "conflicting_extension_selector_preflight", "name_first_extension_module_preflight", "conflicting_extension_module_selector_preflight")) { if ($report.checks.PSObject.Properties.Name -notcontains $check) { throw "$Label report is missing check '$check': $Path" } @@ -488,6 +488,7 @@ try { ) if ($RequireSelectorChainWritePlanComposition) { $writePreflightCommand += "--require-name-first-extension-form" + $writePreflightCommand += "--require-name-first-extension-module" } Invoke-CheckedCommand -Label "REST adapter write-preflight smoke ($currentBaseId)" -Command $writePreflightCommand Assert-WritePreflightReport -Label "REST adapter write-preflight smoke ($currentBaseId)" -Path $writePreflightReport @@ -787,6 +788,7 @@ try { ) if ($RequireSelectorChainWritePlanComposition) { $mcpWritePreflightCommand += "--require-name-first-extension-form" + $mcpWritePreflightCommand += "--require-name-first-extension-module" } Invoke-CheckedCommand -Label "MCP proxy write-preflight smoke ($currentBaseId)" -Command $mcpWritePreflightCommand Assert-WritePreflightReport -Label "MCP proxy write-preflight smoke ($currentBaseId)" -Path $mcpWritePreflightReport diff --git a/tests/1c/test_payload_codec.py b/tests/1c/test_payload_codec.py index 56611ba..0ae4f2d 100644 --- a/tests/1c/test_payload_codec.py +++ b/tests/1c/test_payload_codec.py @@ -11668,6 +11668,64 @@ def test_saved_state_modules_search_scopes_extension_to_cassave_prefix(monkeypat assert seen_rows == [f"{extension_guid}__object-guid.0"] +@pytest.mark.parametrize("requested_extension", [None, "test2"]) +def test_saved_state_modules_search_preserves_public_extension_name( + monkeypatch: pytest.MonkeyPatch, + requested_extension: str | None, +) -> None: + extension_guid = "FB26CF42-7609-11F1-828F-005056B0D483" + file_name = f"{extension_guid.lower()}__form-guid.0" + + monkeypatch.setattr( + adapter_server, + "extension_map_by_guid", + lambda base_id: {extension_guid: {"guid": extension_guid, "name": "test2"}}, + ) + monkeypatch.setattr( + adapter_server, + "storage_files_list", + lambda payload: { + "status": "ok", + "files": [{"FileName": file_name, "PartCount": 1, "Bytes": 1}], + }, + ) + monkeypatch.setattr( + adapter_server, + "saved_state_module_search_row", + lambda **kwargs: { + "table": "ConfigCASSave", + "file_name": file_name, + "owner": {"status": "resolved", "kind": "Catalog", "name": "test2"}, + "form": {"name": "t_Форма"}, + "module": {"kind": "form_module", "name": "Модуль формы"}, + "qualified_name": "test2.t_Форма.Модуль формы", + "streams": [ + { + "preview": "Процедура Команда()\nКонецПроцедуры", + "match": {"query": None, "in_text": False}, + } + ], + }, + ) + + payload = { + "base_id": "upo_test", + "tables": ["ConfigCASSave"], + "limit": 5, + } + if requested_extension: + payload["extension"] = requested_extension + result = adapter_server.metadata_saved_state_modules_search(payload) + + assert result["status"] == "ok" + assert result["counts"]["modules"] == 1 + module = result["modules"][0] + assert module["selector"]["extension"] == "test2" + assert module["streams"][0]["write_plan_target"]["extension"] == "test2" + assert "extension_guid" not in str(module) + assert extension_guid.lower() not in str(module).lower() + + def test_saved_state_forms_search_scopes_extension_to_cassave_prefix(monkeypatch: pytest.MonkeyPatch) -> None: extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483" seen_file_list_payloads = [] @@ -25065,12 +25123,14 @@ def test_public_saved_state_module_search_row_is_name_first() -> None: }, } ], - } + }, + requested_extension="test2", ) assert public is not None assert public["qualified_name"] == "test2.t_Форма.Модуль формы" assert public["selector"] == { + "extension": "test2", "ref": "Catalog.test2", "form": "t_Форма", "module": "Модуль формы", @@ -25079,6 +25139,7 @@ def test_public_saved_state_module_search_row_is_name_first() -> None: assert public["streams"][0]["match"] == {"query": "Команда", "in_text": True, "in_name": False} assert public["streams"][0]["write_plan_target"] == { "kind": "module", + "extension": "test2", "ref": "Catalog.test2", "form": "t_Форма", "module": "Модуль формы", diff --git a/tests/1c/test_write_preflight_smoke.py b/tests/1c/test_write_preflight_smoke.py index 717fd6e..c00d000 100644 --- a/tests/1c/test_write_preflight_smoke.py +++ b/tests/1c/test_write_preflight_smoke.py @@ -112,6 +112,72 @@ def test_public_extension_form_candidate_is_name_first_and_prefers_title() -> No } +def test_public_extension_module_candidate_is_name_first() -> None: + result = smoke.first_public_extension_module_candidate( + { + "modules": [ + { + "streams": [ + { + "preview": "\n&НаКлиенте\nПроцедура Команда()\nКонецПроцедуры", + "write_plan_target": { + "kind": "module", + "extension": "test2", + "ref": "Catalog.test2", + "form": "t_Форма", + "module": "Модуль формы", + "qualified_name": "test2.t_Форма.Модуль формы", + "stream_ordinal": 1, + }, + } + ] + } + ] + } + ) + + assert result == { + "target": { + "kind": "module", + "extension": "test2", + "ref": "Catalog.test2", + "form": "t_Форма", + "module": "Модуль формы", + "qualified_name": "test2.t_Форма.Модуль формы", + "stream_ordinal": 1, + }, + "intent": { + "operation": "replace_with_control", + "control_fragment": "&НаКлиенте", + "new": "&НаКлиенте\nПроцедура Команда()\nКонецПроцедуры", + }, + } + + +def test_public_extension_module_candidate_rejects_storage_handle() -> None: + result = smoke.first_public_extension_module_candidate( + { + "modules": [ + { + "streams": [ + { + "preview": "Процедура Команда()\nКонецПроцедуры", + "write_plan_target": { + "extension": "test2", + "ref": "Catalog.test2", + "module": "Модуль формы", + "module_ref": "ConfigCASSave:opaque.0", + }, + } + ] + } + ] + } + ) + + assert result is None + + def test_different_extension_guid_changes_only_last_hex_digit() -> None: assert ( smoke.different_extension_guid(