From 2f8f11a4d3b32d2f584473caef0ff1fe56ff4371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=82=E9=A9=AC?= Date: Thu, 3 Sep 2026 18:05:36 +0800 Subject: [PATCH 1/2] chore: bump version to 0.15.0 --- desktop/helpers/Cargo.lock | 2 +- desktop/helpers/Cargo.toml | 2 +- desktop/package-lock.json | 4 +- desktop/package.json | 2 +- desktop/src-tauri/Cargo.lock | 4 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- src/iac_code/__init__.py | 2 +- src/iac_code/a2a/events.py | 6 +- src/iac_code/a2a/executor.py | 4 +- src/iac_code/a2a/pipeline_events.py | 3 +- src/iac_code/agent/agent_loop.py | 14 +--- src/iac_code/agui/adapter.py | 6 +- src/iac_code/agui/state.py | 5 +- .../i18n/locales/de/LC_MESSAGES/messages.po | 2 +- .../i18n/locales/es/LC_MESSAGES/messages.po | 2 +- .../i18n/locales/fr/LC_MESSAGES/messages.po | 2 +- .../i18n/locales/ja/LC_MESSAGES/messages.po | 2 +- .../i18n/locales/pt/LC_MESSAGES/messages.po | 2 +- .../i18n/locales/zh/LC_MESSAGES/messages.po | 2 +- src/iac_code/pipeline/constants.py | 4 +- .../pipeline/engine/complete_step_tool.py | 31 +++------ src/iac_code/pipeline/engine/loader.py | 11 +-- .../pipeline/engine/pipeline_runner.py | 4 +- .../pipeline/engine/resume_recovery.py | 6 +- src/iac_code/pipeline/engine/step_executor.py | 12 +--- .../selling_solution_first/hooks/deploying.py | 4 +- .../hooks/materialize_selected_candidate.py | 29 ++++---- .../hooks/solution_planning_and_selection.py | 6 +- .../tools/candidate_planning_records.py | 10 +-- .../tools/show_architecture_plan_tool.py | 4 +- .../tools/show_candidate_detail_tool.py | 8 +-- src/iac_code/providers/dashscope_endpoints.py | 8 +-- src/iac_code/providers/manager.py | 9 +-- src/iac_code/providers/openai_provider.py | 4 +- src/iac_code/providers/qwen_prompts.py | 4 +- src/iac_code/providers/qwen_provider.py | 15 ++-- .../providers/qwen_tool_call_parser.py | 9 +-- src/iac_code/providers/streaming.py | 15 +--- src/iac_code/services/providers/aliyun.py | 4 +- src/iac_code/services/session_backup.py | 8 +-- .../services/session_backup_staging.py | 7 +- .../ros_validation/rules/eip_association.py | 8 +-- src/iac_code/ui/components/select.py | 6 +- src/iac_code/web/events.py | 4 +- src/iac_code/web/pipeline_transcript.py | 3 +- tests/a2a/test_executor.py | 23 ++----- tests/a2a/test_pipeline_identity.py | 18 ++--- tests/a2a/test_pipeline_snapshot.py | 4 +- tests/a2a/test_pipeline_stream.py | 6 +- .../test_start_chat_permission_wait_runner.py | 36 +++++----- tests/agui/test_app.py | 16 ++--- tests/agui/test_http_sse_integration.py | 4 +- tests/agui/test_persistence.py | 51 ++++++++------ .../engine/test_complete_step_tool.py | 4 +- .../pipeline/engine/test_hard_constraints.py | 2 + tests/pipeline/engine/test_step_executor.py | 28 +++++--- .../selling/test_terminal_ui_contract.py | 4 +- .../test_completion_projection.py | 39 ++++------- .../test_materialize_step.py | 14 ++-- .../test_pipeline_definition.py | 15 ++-- .../test_solution_planning_step.py | 26 ++++--- ...st_selling_solution_first_run_scenarios.py | 16 ++--- tests/providers/test_dashscope_provider.py | 8 +-- .../test_provider_model_research_updates.py | 4 +- tests/providers/test_qwen_provider.py | 69 +++++-------------- tests/providers/test_qwen_tool_call_parser.py | 8 +-- .../test_alicloud_ros_agent_bridge.py | 2 +- tests/skill_bridge/test_runtime_release.py | 4 +- .../cloud/aliyun/test_ros_validate_hook.py | 4 +- tests/web/test_diagrams.py | 12 +--- tests/web/test_frontend_static.py | 2 +- tests/web/test_pipeline_transcript.py | 3 +- tests/web/test_session_manager.py | 4 +- 74 files changed, 263 insertions(+), 466 deletions(-) diff --git a/desktop/helpers/Cargo.lock b/desktop/helpers/Cargo.lock index 84befddc..91ba4643 100644 --- a/desktop/helpers/Cargo.lock +++ b/desktop/helpers/Cargo.lock @@ -183,7 +183,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iac-code-desktop-helpers" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "hex", diff --git a/desktop/helpers/Cargo.toml b/desktop/helpers/Cargo.toml index 11f81bed..05886a3b 100644 --- a/desktop/helpers/Cargo.toml +++ b/desktop/helpers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iac-code-desktop-helpers" -version = "0.14.0" +version = "0.15.0" license = "Apache-2.0" edition = "2021" rust-version = "1.77.2" diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 735ab7de..ac3b0a60 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "iac-code-desktop", - "version": "0.14.0", + "version": "0.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "iac-code-desktop", - "version": "0.14.0", + "version": "0.15.0", "devDependencies": { "@tauri-apps/cli": "2.11.2" } diff --git a/desktop/package.json b/desktop/package.json index ce20b3f1..d1417907 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "iac-code-desktop", "private": true, - "version": "0.14.0", + "version": "0.15.0", "scripts": { "tauri": "tauri", "dev": "tauri dev --features updater", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index f6e833f9..2951d6b7 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1577,7 +1577,7 @@ dependencies = [ [[package]] name = "iac-code-desktop" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "fs2", @@ -1605,7 +1605,7 @@ dependencies = [ [[package]] name = "iac-code-desktop-helpers" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "hex", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 3d7be8c9..1ed82ff8 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iac-code-desktop" -version = "0.14.0" +version = "0.15.0" description = "iac-code" authors = ["iac-code contributors"] license = "Apache-2.0" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 57ce694f..7661c5d9 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "iac-code", - "version": "0.14.0", + "version": "0.15.0", "identifier": "com.alibaba.cloud.iaccode.desktop", "build": { "frontendDist": "../bootstrap" diff --git a/src/iac_code/__init__.py b/src/iac_code/__init__.py index d26f4118..d89da73c 100644 --- a/src/iac_code/__init__.py +++ b/src/iac_code/__init__.py @@ -1,2 +1,2 @@ -__version__ = "0.14.0" +__version__ = "0.15.0" __release_date__ = "" diff --git a/src/iac_code/a2a/events.py b/src/iac_code/a2a/events.py index a5e61404..08330a14 100644 --- a/src/iac_code/a2a/events.py +++ b/src/iac_code/a2a/events.py @@ -516,11 +516,7 @@ async def publish_stream_event( task_id=task_id, context_id=context_id, state=TaskState.TASK_STATE_WORKING, - metadata={ - "iac_code": { - "usage": usage - } - }, + metadata={"iac_code": {"usage": usage}}, iac_code_session_id=iac_code_session_id, ) return None diff --git a/src/iac_code/a2a/executor.py b/src/iac_code/a2a/executor.py index 6abcba1f..bfb0d893 100644 --- a/src/iac_code/a2a/executor.py +++ b/src/iac_code/a2a/executor.py @@ -2923,9 +2923,7 @@ def _read(name: str) -> str | None: return None if re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", region_id) is None: language = self._resolve_preferred_language(metadata) or "en" - raise InvalidParamsError( - translate_message("Unsupported Alibaba Cloud region ID.", language=language) - ) + raise InvalidParamsError(translate_message("Unsupported Alibaba Cloud region ID.", language=language)) configured = AliyunCredentials.load() if configured is None: return None diff --git a/src/iac_code/a2a/pipeline_events.py b/src/iac_code/a2a/pipeline_events.py index 42218023..0b7d003d 100644 --- a/src/iac_code/a2a/pipeline_events.py +++ b/src/iac_code/a2a/pipeline_events.py @@ -561,8 +561,7 @@ def _completion_artifact_events(self, completed: dict[str, Any]) -> list[dict[st if conditions and ( not isinstance(conclusion, dict) or not all( - _resolve_artifact_expression(conclusion, field) == value - for field, value in conditions.items() + _resolve_artifact_expression(conclusion, field) == value for field, value in conditions.items() ) ): continue diff --git a/src/iac_code/agent/agent_loop.py b/src/iac_code/agent/agent_loop.py index 419d4b76..77250c66 100644 --- a/src/iac_code/agent/agent_loop.py +++ b/src/iac_code/agent/agent_loop.py @@ -1787,9 +1787,7 @@ def _prepare_request_lease( tools = list(self.tool_registry.list_tools()) tool_definitions = self._get_tool_definitions(tools) begin_request = ( - getattr(request_manager, "begin_request", None) - if hasattr(type(request_manager), "begin_request") - else None + getattr(request_manager, "begin_request", None) if hasattr(type(request_manager), "begin_request") else None ) lease = begin_request(base_system_prompt, tool_definitions or None) if callable(begin_request) else None try: @@ -2820,9 +2818,7 @@ async def _complete_provider_with_lease( cache_policy: str | None = None, ) -> Any: begin_request = ( - getattr(request_manager, "begin_request", None) - if hasattr(type(request_manager), "begin_request") - else None + getattr(request_manager, "begin_request", None) if hasattr(type(request_manager), "begin_request") else None ) lease = begin_request(system, tools) if callable(begin_request) else None effective_system = getattr(lease, "system_prompt", system) @@ -3061,11 +3057,7 @@ def _record_session_usage( # Compatibility for lightweight third-party/test managers that do # not implement ProviderManager's internal attribution contract. provider = self._get_runtime_provider_key() - model = ( - self._provider_manager.get_model_name() - if hasattr(self._provider_manager, "get_model_name") - else "" - ) + model = self._provider_manager.get_model_name() if hasattr(self._provider_manager, "get_model_name") else "" try: self._session_usage_store.append( self._cwd, diff --git a/src/iac_code/agui/adapter.py b/src/iac_code/agui/adapter.py index eb0684d1..035bfd35 100644 --- a/src/iac_code/agui/adapter.py +++ b/src/iac_code/agui/adapter.py @@ -384,8 +384,7 @@ async def stream(self, ticket: RunTicket) -> AsyncIterator[Any]: pending_values = [ value for value in a2a_inputs(event) - if (binding.execution_id, str(value.get("inputId") or "")) - not in binding.applied_resume_digests + if (binding.execution_id, str(value.get("inputId") or "")) not in binding.applied_resume_digests ] if pending_values: if sideband_recovery_after is not None: @@ -911,8 +910,7 @@ async def _prepare_resume( # it accepts or rejects the response. Terminal task states always win. task_is_terminal = a2a_state(task) in _FAILED_STATES | {"canceled", "completed"} replace_pending = task_is_terminal or ( - not resumed_pending_ids - and ("input" in task_metadata or "pendingPermissions" in task_metadata) + not resumed_pending_ids and ("input" in task_metadata or "pendingPermissions" in task_metadata) ) self._merge_pending( binding, diff --git a/src/iac_code/agui/state.py b/src/iac_code/agui/state.py index c8665dfd..4fde0fe5 100644 --- a/src/iac_code/agui/state.py +++ b/src/iac_code/agui/state.py @@ -73,10 +73,7 @@ def load_thread(self, thread_id: str) -> dict[str, Any] | None: def save_thread(self, thread_id: str, state: Mapping[str, Any]) -> None: document = dict(state) - if ( - document.get("schemaVersion") != AGUI_STATE_SCHEMA_VERSION - or document.get("threadId") != thread_id - ): + if document.get("schemaVersion") != AGUI_STATE_SCHEMA_VERSION or document.get("threadId") != thread_id: raise AguiStateStoreError("Refusing to save invalid AG-UI thread state.") try: ensure_private_dir(self.state_dir) diff --git a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po index f4460fb6..ea757dc9 100644 --- a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.14.0\n" +"Project-Id-Version: iac-code 0.15.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-04 15:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" diff --git a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po index 23ea221b..e4803c69 100644 --- a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.14.0\n" +"Project-Id-Version: iac-code 0.15.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-04 15:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" diff --git a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po index 5a7265a0..8bc5c9bb 100644 --- a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.14.0\n" +"Project-Id-Version: iac-code 0.15.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-04 15:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" diff --git a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po index bbae7f06..24ee501e 100644 --- a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.14.0\n" +"Project-Id-Version: iac-code 0.15.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-04 15:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" diff --git a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po index aa719967..18c99bcc 100644 --- a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.14.0\n" +"Project-Id-Version: iac-code 0.15.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-04 15:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" diff --git a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po index ded27c54..32331be4 100644 --- a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.14.0\n" +"Project-Id-Version: iac-code 0.15.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-04 15:58+0800\n" "PO-Revision-Date: 2026-04-02 00:00+0000\n" diff --git a/src/iac_code/pipeline/constants.py b/src/iac_code/pipeline/constants.py index c6dcf34a..635a2b63 100644 --- a/src/iac_code/pipeline/constants.py +++ b/src/iac_code/pipeline/constants.py @@ -4,9 +4,7 @@ SELLING_SOLUTION_FIRST_PIPELINE_NAME = "selling_solution_first" #: Pipelines a remote caller (POP → ros-ai-agent → A2A) may select per request. -SELECTABLE_PIPELINE_NAMES: frozenset[str] = frozenset( - {SELLING_PIPELINE_NAME, SELLING_SOLUTION_FIRST_PIPELINE_NAME} -) +SELECTABLE_PIPELINE_NAMES: frozenset[str] = frozenset({SELLING_PIPELINE_NAME, SELLING_SOLUTION_FIRST_PIPELINE_NAME}) CLEANUP_PROMPT_METADATA_TYPE = "pipeline_cleanup_prompt" diff --git a/src/iac_code/pipeline/engine/complete_step_tool.py b/src/iac_code/pipeline/engine/complete_step_tool.py index ddf69637..2825ace0 100644 --- a/src/iac_code/pipeline/engine/complete_step_tool.py +++ b/src/iac_code/pipeline/engine/complete_step_tool.py @@ -418,7 +418,7 @@ def _raw_completion_input_schema(self) -> dict[str, Any]: schema = copy.deepcopy(self.input_schema) schema["description"] = _( 'complete_step arguments must be {"conclusion": {...}}; keep all conclusion fields, ' - 'including candidates, inside conclusion and do not submit them at the tool input top level.' + "including candidates, inside conclusion and do not submit them at the tool input top level." ) properties = schema.get("properties") if isinstance(properties, dict) and self._step_config.completion_input_schema: @@ -454,9 +454,7 @@ def _completion_input_error_detail(self, error: jsonschema.ValidationError) -> d "validator": str(error.validator or ""), "message": self._bounded_completion_validation_message(error), "expected": self._bounded_completion_expected(error.validator, error.validator_value), - "description": self._bounded_description( - self._nearest_completion_input_description(absolute_path, error) - ), + "description": self._bounded_description(self._nearest_completion_input_description(absolute_path, error)), "received": self._bounded_received(error.instance), } @@ -758,9 +756,7 @@ def _conclusion_schema_error_detail( "validator": str(error.validator or ""), "message": self._bounded_completion_validation_message(error), "expected": self._bounded_completion_expected(error.validator, error.validator_value), - "description": self._bounded_description( - self._nearest_schema_description(schema, path_parts, error) - ), + "description": self._bounded_description(self._nearest_schema_description(schema, path_parts, error)), "received": self._bounded_received(error.instance), } @@ -914,22 +910,19 @@ def _validate_structured_user_input_action( "input_type": "structured", "user_input": self._user_message, "parameter_overrides": ( - structured.parameter_overrides - if structured.parameter_overrides_provided - else current_overrides + structured.parameter_overrides if structured.parameter_overrides_provided else current_overrides ), } if confirmation != expected_confirmation: - return _( - "{message} complete_step.conclusion.{field} must record the exact structured input." - ).format(message=base_message, field=confirmation_field) + return _("{message} complete_step.conclusion.{field} must record the exact structured input.").format( + message=base_message, field=confirmation_field + ) change_required_actions = self._expected_actions( {"actions": requirement.get("require_parameter_changes_for_actions")} ) if structured.action in change_required_actions and ( - not structured.parameter_overrides_provided - or structured.parameter_overrides == current_overrides + not structured.parameter_overrides_provided or structured.parameter_overrides == current_overrides ): return base_message @@ -1549,9 +1542,7 @@ def finalize_completion_input( enriched = enricher( tool_input=copy.deepcopy(normalized_input), context_snapshot=copy.deepcopy(self._completion_guard_state.get("context_snapshot") or {}), - tool_result_records=copy.deepcopy( - self._completion_guard_state.get("tool_result_records") or [] - ), + tool_result_records=copy.deepcopy(self._completion_guard_state.get("tool_result_records") or []), user_message=self._user_message, completion_guard_state=self._completion_guard_state, config=self._step_config, @@ -1769,9 +1760,7 @@ def _string_set(value: Any) -> set[str]: async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult: del context submitted_delta = copy.deepcopy(tool_input) - projection_metadata = ( - {"submitted_delta": submitted_delta} if self._step_config.completion_input_schema else {} - ) + projection_metadata = {"submitted_delta": submitted_delta} if self._step_config.completion_input_schema else {} logger.debug( "[complete_step] step=%s input=%s", diff --git a/src/iac_code/pipeline/engine/loader.py b/src/iac_code/pipeline/engine/loader.py index 964b58b0..6ed537c8 100644 --- a/src/iac_code/pipeline/engine/loader.py +++ b/src/iac_code/pipeline/engine/loader.py @@ -347,9 +347,7 @@ def _parse_surface_overrides(raw: object, step_id: str) -> dict[str, StepSurface conclusion_schema = override.get("conclusion_schema") if conclusion_schema is not None and not isinstance(conclusion_schema, dict): - raise ValueError( - f"Step '{step_id}': surface_overrides.{surface}.conclusion_schema must be a mapping" - ) + raise ValueError(f"Step '{step_id}': surface_overrides.{surface}.conclusion_schema must be a mapping") overrides[surface] = StepSurfaceOverride( prompt_file=prompt, @@ -384,8 +382,7 @@ def _parse_a2a_artifacts(raw: object, step_id: str) -> list[A2AArtifactSpec]: has_content_from_file = isinstance(content_from_file, str) and bool(content_from_file) if has_content == has_content_from_file: raise ValueError( - f"Step '{step_id}': a2a_artifacts[{index}] must define exactly one of " - "content or content_from_file" + f"Step '{step_id}': a2a_artifacts[{index}] must define exactly one of content or content_from_file" ) if not isinstance(media_type, str) or not media_type: raise ValueError(f"Step '{step_id}': a2a_artifacts[{index}].media_type must be a non-empty string") @@ -396,9 +393,7 @@ def _parse_a2a_artifacts(raw: object, step_id: str) -> list[A2AArtifactSpec]: raw_when_equals = item.get("when_conclusion_field_equals") when_equals = {} if raw_when_equals is None else raw_when_equals if not isinstance(when_equals, dict) or not all(isinstance(key, str) and key for key in when_equals): - raise ValueError( - f"Step '{step_id}': a2a_artifacts[{index}].when_conclusion_field_equals must be a mapping" - ) + raise ValueError(f"Step '{step_id}': a2a_artifacts[{index}].when_conclusion_field_equals must be a mapping") specs.append( A2AArtifactSpec( path=path, diff --git a/src/iac_code/pipeline/engine/pipeline_runner.py b/src/iac_code/pipeline/engine/pipeline_runner.py index b8caf669..1f74262d 100644 --- a/src/iac_code/pipeline/engine/pipeline_runner.py +++ b/src/iac_code/pipeline/engine/pipeline_runner.py @@ -2953,9 +2953,7 @@ async def resume( and step.config.get("confirmation_accepts_parameter_overrides") is True and step.validate_structured_confirmation is not None ): - validation_message = self._structured_confirmation_validation_message( - step, current_conclusion, user_text - ) + validation_message = self._structured_confirmation_validation_message(step, current_conclusion, user_text) if validation_message: # The submitted parameters are illegal, so the step keeps its waiting input untouched: no # bookkeeping is popped, no state is saved and no model turn is spent. diff --git a/src/iac_code/pipeline/engine/resume_recovery.py b/src/iac_code/pipeline/engine/resume_recovery.py index 3379b2a3..e271936f 100644 --- a/src/iac_code/pipeline/engine/resume_recovery.py +++ b/src/iac_code/pipeline/engine/resume_recovery.py @@ -51,11 +51,7 @@ def _without_overridden_error_results(message: Message, successful_ids: set[str] content = [ block for block in message.content - if not ( - isinstance(block, ToolResultBlock) - and block.is_error - and block.tool_use_id in successful_ids - ) + if not (isinstance(block, ToolResultBlock) and block.is_error and block.tool_use_id in successful_ids) ] if not content: return None diff --git a/src/iac_code/pipeline/engine/step_executor.py b/src/iac_code/pipeline/engine/step_executor.py index d61bdab3..02485392 100644 --- a/src/iac_code/pipeline/engine/step_executor.py +++ b/src/iac_code/pipeline/engine/step_executor.py @@ -526,9 +526,7 @@ def build_agent_loop_context( completion_guard_state: dict[str, Any] = ensure_completion_guard_state( reconstruct_completion_guard_state( repaired_messages, - completion_record_contract=self._optional_config_string( - step.config.get("completion_record_contract") - ), + completion_record_contract=self._optional_config_string(step.config.get("completion_record_contract")), ) ) saved_step_conclusion = context.snapshot().get(step.conclusion_field) @@ -700,9 +698,7 @@ def finalize_completion_input_from_transcript( completion_guard_state = ensure_completion_guard_state( reconstruct_completion_guard_state( list(resume_messages), - completion_record_contract=self._optional_config_string( - step.config.get("completion_record_contract") - ), + completion_record_contract=self._optional_config_string(step.config.get("completion_record_contract")), ) ) if self._cwd: @@ -1196,9 +1192,7 @@ def _build_step_tools( authoritative_candidate_targets=self._config_string_tuple( step.config.get("authoritative_candidate_targets") ), - completion_record_contract=self._optional_config_string( - step.config.get("completion_record_contract") - ), + completion_record_contract=self._optional_config_string(step.config.get("completion_record_contract")), hard_constraint_evidence_contract=self._optional_config_string( step.config.get("hard_constraint_evidence_contract") ), diff --git a/src/iac_code/pipeline/selling_solution_first/hooks/deploying.py b/src/iac_code/pipeline/selling_solution_first/hooks/deploying.py index b794a578..66fa5bc0 100644 --- a/src/iac_code/pipeline/selling_solution_first/hooks/deploying.py +++ b/src/iac_code/pipeline/selling_solution_first/hooks/deploying.py @@ -52,9 +52,7 @@ def enrich_completion_input( tool_input["conclusion"] = {"status": "cancelled"} return tool_input records = [ - record - for record in tool_result_records - if isinstance(record, dict) and record.get("tool_name") == "ros_deploy" + record for record in tool_result_records if isinstance(record, dict) and record.get("tool_name") == "ros_deploy" ] if status == "success": record = next( diff --git a/src/iac_code/pipeline/selling_solution_first/hooks/materialize_selected_candidate.py b/src/iac_code/pipeline/selling_solution_first/hooks/materialize_selected_candidate.py index 2e828206..4a64e3d8 100644 --- a/src/iac_code/pipeline/selling_solution_first/hooks/materialize_selected_candidate.py +++ b/src/iac_code/pipeline/selling_solution_first/hooks/materialize_selected_candidate.py @@ -219,9 +219,7 @@ def enrich_completion_input( records, tool_name="ros_preview_template", after=last_mutation, - predicate=lambda record: _record_matches_anchor( - record, canonical_output, effective_parameters, region, cwd - ), + predicate=lambda record: _record_matches_anchor(record, canonical_output, effective_parameters, region, cwd), ) preview_validation = _preview_projection(preview, output_path, effective_parameters, region) quote = _quote_projection(anchor) @@ -615,9 +613,7 @@ def _quote_projection(anchor: dict[str, Any]) -> dict[str, Any]: } currency = result.get("Currency") or (next(iter(resource_currencies)) if len(resource_currencies) == 1 else "CNY") if str(currency).upper() != "CNY": - return _unavailable_quote( - _("Unsupported ROS estimate currency: {currency}").format(currency=currency) - ) + return _unavailable_quote(_("Unsupported ROS estimate currency: {currency}").format(currency=currency)) if any(item != "CNY" for item in resource_currencies): return _unavailable_quote( _("Unsupported ROS estimate resource currencies: {currencies}").format( @@ -811,11 +807,15 @@ def _confirmation_options(selection: Any) -> list[dict[str, str]]: }, } raw_candidates = selection.get("candidates") if isinstance(selection, dict) else None - actions = ["confirm", "cancel"] if not isinstance(raw_candidates, list) or len(raw_candidates) <= 1 else [ - "confirm", - "reselect", - "cancel", - ] + actions = ( + ["confirm", "cancel"] + if not isinstance(raw_candidates, list) or len(raw_candidates) <= 1 + else [ + "confirm", + "reselect", + "cancel", + ] + ) return [copy.deepcopy(defaults[action]) for action in actions] @@ -1028,8 +1028,11 @@ def _project_evidence( def _allowed_context_path(path: str, allowed: tuple[str, ...]) -> bool: - return bool(path) and ".." not in path and "*" not in path and any( - path == prefix or path.startswith(prefix + ".") for prefix in allowed + return ( + bool(path) + and ".." not in path + and "*" not in path + and any(path == prefix or path.startswith(prefix + ".") for prefix in allowed) ) diff --git a/src/iac_code/pipeline/selling_solution_first/hooks/solution_planning_and_selection.py b/src/iac_code/pipeline/selling_solution_first/hooks/solution_planning_and_selection.py index 699137a5..cff6337c 100644 --- a/src/iac_code/pipeline/selling_solution_first/hooks/solution_planning_and_selection.py +++ b/src/iac_code/pipeline/selling_solution_first/hooks/solution_planning_and_selection.py @@ -347,9 +347,7 @@ def _validate_candidate_resource_intents( if missing: shown = missing[:5] suffix = ( - _("; {count} more omitted").format(count=len(missing) - len(shown)) - if len(missing) > len(shown) - else "" + _("; {count} more omitted").format(count=len(missing) - len(shown)) if len(missing) > len(shown) else "" ) raise CompletionEnrichmentError( _( @@ -361,7 +359,7 @@ def _validate_candidate_resource_intents( def _candidate_slug(name: str) -> str: slug = re.sub(r"[^a-z0-9]+", "-", name.casefold()).strip("-") - return (slug[:48].rstrip("-") or "solution") + return slug[:48].rstrip("-") or "solution" def _candidate_products(candidate: dict[str, Any]) -> list[str]: diff --git a/src/iac_code/pipeline/selling_solution_first/tools/candidate_planning_records.py b/src/iac_code/pipeline/selling_solution_first/tools/candidate_planning_records.py index 56882c83..8058e0ee 100644 --- a/src/iac_code/pipeline/selling_solution_first/tools/candidate_planning_records.py +++ b/src/iac_code/pipeline/selling_solution_first/tools/candidate_planning_records.py @@ -43,9 +43,7 @@ def latest_candidate_outline_batch(records: list[dict[str, Any]]) -> CandidateOu continue sequence = _record_sequence(record, position) recorded_candidate_set_id = record.get("candidate_set_id") - candidate_set_id = ( - str(recorded_candidate_set_id).strip() if isinstance(recorded_candidate_set_id, str) else "" - ) + candidate_set_id = str(recorded_candidate_set_id).strip() if isinstance(recorded_candidate_set_id, str) else "" record_id = record.get("record_id") if not candidate_set_id: candidate_set_id = str(record_id).strip() if isinstance(record_id, str) else "" @@ -54,11 +52,7 @@ def latest_candidate_outline_batch(records: list[dict[str, Any]]) -> CandidateOu # An identical repeated call is recorded as a successful idempotent observation with the # original candidateSetId. It must not move the active batch boundary forward, otherwise # details already produced for that batch would be incorrectly invalidated. - if ( - latest is not None - and latest.candidate_set_id == candidate_set_id - and latest.candidates == candidates - ): + if latest is not None and latest.candidate_set_id == candidate_set_id and latest.candidates == candidates: continue latest = CandidateOutlineBatch( candidate_set_id=candidate_set_id, diff --git a/src/iac_code/pipeline/selling_solution_first/tools/show_architecture_plan_tool.py b/src/iac_code/pipeline/selling_solution_first/tools/show_architecture_plan_tool.py index 2325ac6c..d74d8e7a 100644 --- a/src/iac_code/pipeline/selling_solution_first/tools/show_architecture_plan_tool.py +++ b/src/iac_code/pipeline/selling_solution_first/tools/show_architecture_plan_tool.py @@ -152,9 +152,7 @@ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> "Displayed {count} candidate outlines; candidateSetId={candidate_set_id}. " "Do not repeat show_architecture_plan unless the user changes the candidate set; " "continue with show_candidate_detail." - ).format( - count=len(candidates), candidate_set_id=candidate_set_id - ), + ).format(count=len(candidates), candidate_set_id=candidate_set_id), metadata={"candidate_set_id": candidate_set_id}, ) diff --git a/src/iac_code/pipeline/selling_solution_first/tools/show_candidate_detail_tool.py b/src/iac_code/pipeline/selling_solution_first/tools/show_candidate_detail_tool.py index 469f436f..bcc4bc77 100644 --- a/src/iac_code/pipeline/selling_solution_first/tools/show_candidate_detail_tool.py +++ b/src/iac_code/pipeline/selling_solution_first/tools/show_candidate_detail_tool.py @@ -177,9 +177,7 @@ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> batch = latest_candidate_outline_batch(records) if batch is None: return ToolResult.error( - _( - "show_candidate_detail is not allowed before a successful show_architecture_plan outline batch." - ) + _("show_candidate_detail is not allowed before a successful show_architecture_plan outline batch.") ) expected_index = first_missing_candidate_detail_index(records, batch) @@ -211,9 +209,7 @@ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ) try: - mermaid_source, architecture_context, warnings = render_architecture_graph( - tool_input.get("topology_graph") - ) + mermaid_source, architecture_context, warnings = render_architecture_graph(tool_input.get("topology_graph")) except ValueError as exc: return ToolResult( content=_("Failed to render the candidate topology: {reason}").format(reason=str(exc)), diff --git a/src/iac_code/providers/dashscope_endpoints.py b/src/iac_code/providers/dashscope_endpoints.py index c7371ff6..85f54cad 100644 --- a/src/iac_code/providers/dashscope_endpoints.py +++ b/src/iac_code/providers/dashscope_endpoints.py @@ -20,9 +20,7 @@ "coding.dashscope.aliyuncs.com": "aliyun_codingplan", "coding-intl.dashscope.aliyuncs.com": "aliyun_codingplan_intl", } -_TOKEN_PLAN_REGIONS = frozenset( - {"cn-beijing", "ap-southeast-1", "ap-northeast-1", "eu-central-1", "us-east-1"} -) +_TOKEN_PLAN_REGIONS = frozenset({"cn-beijing", "ap-southeast-1", "ap-northeast-1", "eu-central-1", "us-east-1"}) def _parsed_https_endpoint(base_url: str | None) -> tuple[str, str] | None: @@ -83,9 +81,7 @@ def is_bailian_compatible_endpoint(base_url: str | None) -> bool: and _is_dns_label(labels[0]) and labels[1] in _TOKEN_PLAN_REGIONS ) - return is_maas and ( - _path_matches(path, "/compatible-mode") or _path_matches(path, "/apps/anthropic") - ) + return is_maas and (_path_matches(path, "/compatible-mode") or _path_matches(path, "/apps/anthropic")) def _is_dns_label(value: str) -> bool: diff --git a/src/iac_code/providers/manager.py b/src/iac_code/providers/manager.py index 8cc59205..547d3a1f 100644 --- a/src/iac_code/providers/manager.py +++ b/src/iac_code/providers/manager.py @@ -135,6 +135,7 @@ def _is_retryable_provider_error(exc: BaseException) -> bool: } ) + class _BestEffortSpan: def __init__(self, span: Any | None = None) -> None: self._span = span @@ -814,9 +815,7 @@ def _telemetry_provider_name(provider: Any) -> str: def _provider_telemetry_attrs(provider: Any) -> dict[str, str | bool]: attrs: dict[str, str | bool] = { - IacCodeAttr.OFFICIAL_ENDPOINT: official_dashscope_wire_provider_key( - _provider_endpoint_url(provider) - ) + IacCodeAttr.OFFICIAL_ENDPOINT: official_dashscope_wire_provider_key(_provider_endpoint_url(provider)) is not None, } adapter_name = _string_provider_attr(provider, "_ADAPTER_NAME") @@ -1790,9 +1789,7 @@ def commit_replay_failure( yield terminal_event return yield replay_event - raise UnsafeStreamProtocolError( - "Qwen replay ended before message completion." - ) + raise UnsafeStreamProtocolError("Qwen replay ended before message completion.") except UnsafeStreamProtocolError as exc: last_unsafe_error = exc commit_replay_failure(exc) diff --git a/src/iac_code/providers/openai_provider.py b/src/iac_code/providers/openai_provider.py index 36c0cd02..4c33d8c7 100644 --- a/src/iac_code/providers/openai_provider.py +++ b/src/iac_code/providers/openai_provider.py @@ -197,9 +197,7 @@ def _extract_reasoning_text(self, message_or_delta: Any) -> str: reasoning = getattr(message_or_delta, "reasoning_content", None) return reasoning if isinstance(reasoning, str) else "" - def _create_stream_response_adapter( - self, tools: list[ToolDefinition] | None - ) -> OpenAIStreamResponseAdapter: + def _create_stream_response_adapter(self, tools: list[ToolDefinition] | None) -> OpenAIStreamResponseAdapter: return OpenAIStreamResponseAdapter(self, tools) # -- Message conversion ---------------------------------------------------- diff --git a/src/iac_code/providers/qwen_prompts.py b/src/iac_code/providers/qwen_prompts.py index 387231ec..220185f5 100644 --- a/src/iac_code/providers/qwen_prompts.py +++ b/src/iac_code/providers/qwen_prompts.py @@ -25,9 +25,7 @@ def prepare_qwen_system_prompt(system: str, model: str, tools: list[ToolDefiniti parameter_name = next(iter(properties), None) if isinstance(properties, dict) else None normalized = normalized_model_name(model) if "coder" in normalized: - parameter = ( - f"\nVALUE" if isinstance(parameter_name, str) else "" - ) + parameter = f"\nVALUE" if isinstance(parameter_name, str) else "" example = f"{parameter}" elif "-vl" in normalized or normalized.endswith("vl") or "qwen-vl" in normalized: arguments = {parameter_name: "VALUE"} if isinstance(parameter_name, str) else {} diff --git a/src/iac_code/providers/qwen_provider.py b/src/iac_code/providers/qwen_provider.py index b5061abf..176b652d 100644 --- a/src/iac_code/providers/qwen_provider.py +++ b/src/iac_code/providers/qwen_provider.py @@ -118,14 +118,11 @@ def _build_thinking_kwargs_with_mandatory(self, mandatory: bool) -> dict[str, An disabled = ( not resolved_enabled if resolved_enabled is not None - else self._thinking_disabled() - or effort in {"none", "off", "disable", "disabled", "false", "0"} + else self._thinking_disabled() or effort in {"none", "off", "disable", "disabled", "false", "0"} ) effort_is_disable = effort in {"none", "off", "disable", "disabled", "false", "0"} concrete_priority = ( - self._thinking_intent.effort.priority - if dominant == "effort" - else self._thinking_intent.budget.priority + self._thinking_intent.effort.priority if dominant == "effort" else self._thinking_intent.budget.priority ) if ( dominant in {"effort", "budget"} @@ -176,15 +173,11 @@ def _build_legacy_qwen_thinking_kwargs(self, spec: Any, *, mandatory: bool) -> d effort_is_disable = effort in {"none", "off", "disable", "disabled", "false", "0"} resolved_enabled = self._thinking_intent.enabled.value disabled = ( - not resolved_enabled - if resolved_enabled is not None - else self._thinking_disabled() or effort_is_disable + not resolved_enabled if resolved_enabled is not None else self._thinking_disabled() or effort_is_disable ) dominant = self._thinking_intent.dominant_concrete_field() concrete_priority = ( - self._thinking_intent.effort.priority - if dominant == "effort" - else self._thinking_intent.budget.priority + self._thinking_intent.effort.priority if dominant == "effort" else self._thinking_intent.budget.priority ) if ( dominant in {"effort", "budget"} diff --git a/src/iac_code/providers/qwen_tool_call_parser.py b/src/iac_code/providers/qwen_tool_call_parser.py index c3b713d2..043b6d0d 100644 --- a/src/iac_code/providers/qwen_tool_call_parser.py +++ b/src/iac_code/providers/qwen_tool_call_parser.py @@ -299,8 +299,7 @@ def recover_xml_tool_calls(text: str, tools: list[ToolDefinition] | None) -> Xml contained = [ item for item in recovered_ranges - if wrapper_start + wrapper.start("body") <= item[0] - and item[1] <= wrapper_start + wrapper.end("body") + if wrapper_start + wrapper.start("body") <= item[0] and item[1] <= wrapper_start + wrapper.end("body") ] if not contained: return None @@ -385,11 +384,7 @@ def _position_inside_markdown_fence( length = len(match.group(1)) if open_fence is None: open_fence = (delimiter, length) - elif ( - open_fence[0] == delimiter - and length >= open_fence[1] - and not line[match.end() :].strip() - ): + elif open_fence[0] == delimiter and length >= open_fence[1] and not line[match.end() :].strip(): open_fence = None return open_fence is not None diff --git a/src/iac_code/providers/streaming.py b/src/iac_code/providers/streaming.py index 2c842c72..e87b720f 100644 --- a/src/iac_code/providers/streaming.py +++ b/src/iac_code/providers/streaming.py @@ -332,11 +332,7 @@ def _process_fence_line(self, line: str) -> None: if self._fence_delimiter is None: self._fence_delimiter = delimiter self._fence_length = length - elif ( - self._fence_delimiter == delimiter - and length >= self._fence_length - and not line[match.end() :].strip() - ): + elif self._fence_delimiter == delimiter and length >= self._fence_length and not line[match.end() :].strip(): self._fence_delimiter = None self._fence_length = 0 @@ -393,10 +389,7 @@ def finalize_tags( raise UnsafeStreamProtocolError("Qwen emitted an unsafe or conflicting thinking-tag block.") closing = self._STANDALONE_CLOSING.fullmatch(literal) safe_closing = ( - finish_reason == "tool_calls" - and bool(native_calls) - and not reasoning_has_tag - and closing is not None + finish_reason == "tool_calls" and bool(native_calls) and not reasoning_has_tag and closing is not None ) if safe_closing: return [], True @@ -449,9 +442,7 @@ def feed(self, delta: Any, finish_reason: str | None) -> list[StreamEvent]: else: last_open = tag_probe.rfind("<") possible = tag_probe[last_open:] if last_open >= 0 else "" - self._reasoning_tag_probe = ( - possible if self._guard._is_possible_tag_prefix(possible) else "" - ) + self._reasoning_tag_probe = possible if self._guard._is_possible_tag_prefix(possible) else "" events.append(ThinkingDeltaEvent(text=normalized_reasoning)) content = getattr(delta, "content", None) normalized_content = self._content_normalizer.feed(content) if isinstance(content, str) and content else "" diff --git a/src/iac_code/services/providers/aliyun.py b/src/iac_code/services/providers/aliyun.py index a66f6d59..e0119007 100644 --- a/src/iac_code/services/providers/aliyun.py +++ b/src/iac_code/services/providers/aliyun.py @@ -298,9 +298,7 @@ def refresh_oauth_if_needed( owns_client = oauth_client is None client = ( - AliyunOAuthClient(get_oauth_site(credential.oauth_site_type)) - if oauth_client is None - else oauth_client + AliyunOAuthClient(get_oauth_site(credential.oauth_site_type)) if oauth_client is None else oauth_client ) try: diff --git a/src/iac_code/services/session_backup.py b/src/iac_code/services/session_backup.py index 5d7e0c9f..5fea3052 100644 --- a/src/iac_code/services/session_backup.py +++ b/src/iac_code/services/session_backup.py @@ -423,9 +423,7 @@ def reconcile_session( minimum_generation: int | None = None, ) -> SessionReconcileResult: if minimum_generation is not None and ( - isinstance(minimum_generation, bool) - or not isinstance(minimum_generation, int) - or minimum_generation <= 0 + isinstance(minimum_generation, bool) or not isinstance(minimum_generation, int) or minimum_generation <= 0 ): raise ValueError("minimum_generation must be a positive integer") if not self._backup_enabled(): @@ -450,9 +448,7 @@ def reconcile_session( if shared is not None else None ) - if minimum_generation is not None and ( - local_state is None or local_state.generation < minimum_generation - ): + if minimum_generation is not None and (local_state is None or local_state.generation < minimum_generation): if shared_state is None or shared_state.generation < minimum_generation: raise SessionBackupNotReadyError( minimum_generation=minimum_generation, diff --git a/src/iac_code/services/session_backup_staging.py b/src/iac_code/services/session_backup_staging.py index fc0f4cff..fd2bdc3e 100644 --- a/src/iac_code/services/session_backup_staging.py +++ b/src/iac_code/services/session_backup_staging.py @@ -149,8 +149,7 @@ def backup_session( existing = self._read_existing_snapshot_state(destination, session_id) if existing is not None: completed_next = ( - base_state.status == "succeeded" - and existing.parent_generation == base_state.generation + base_state.status == "succeeded" and existing.parent_generation == base_state.generation ) if not completed_next and not existing.same_lineage(committed_state): raise SessionBackupConflict( @@ -249,9 +248,7 @@ def reconcile_session( minimum_generation: int | None = None, ) -> SessionReconcileResult: if minimum_generation is not None and ( - isinstance(minimum_generation, bool) - or not isinstance(minimum_generation, int) - or minimum_generation <= 0 + isinstance(minimum_generation, bool) or not isinstance(minimum_generation, int) or minimum_generation <= 0 ): raise ValueError("minimum_generation must be a positive integer") if not self._backup_enabled(): diff --git a/src/iac_code/tools/cloud/aliyun/ros_validation/rules/eip_association.py b/src/iac_code/tools/cloud/aliyun/ros_validation/rules/eip_association.py index 567f166b..3711e05f 100644 --- a/src/iac_code/tools/cloud/aliyun/ros_validation/rules/eip_association.py +++ b/src/iac_code/tools/cloud/aliyun/ros_validation/rules/eip_association.py @@ -65,9 +65,7 @@ def check(self, context: Any) -> tuple[Diagnostic, ...]: ecs_resources = { name: definition for name, definition in resources.items() - if isinstance(name, str) - and isinstance(definition, Mapping) - and definition.get("Type") in _ECS_TYPES + if isinstance(name, str) and isinstance(definition, Mapping) and definition.get("Type") in _ECS_TYPES } diagnostics: list[Diagnostic] = [] for association_name, association in resources.items(): @@ -83,9 +81,7 @@ def check(self, context: Any) -> tuple[Diagnostic, ...]: target = ecs_resources[target_name] target_properties = target.get("Properties") allocate_public_ip = ( - target_properties.get("AllocatePublicIP") - if isinstance(target_properties, Mapping) - else None + target_properties.get("AllocatePublicIP") if isinstance(target_properties, Mapping) else None ) if _is_explicit_false(allocate_public_ip): continue diff --git a/src/iac_code/ui/components/select.py b/src/iac_code/ui/components/select.py index 7733415b..cdf582ad 100644 --- a/src/iac_code/ui/components/select.py +++ b/src/iac_code/ui/components/select.py @@ -184,9 +184,9 @@ def handle_key(self, key_event: KeyEvent) -> bool: # Optional type-to-edit behavior for selectors whose final row is an inline input. focused_option = self._options[self.state.focused_index] if self._options else None - direct_text = ( - len(key_event.char) == 1 and key_event.char.isprintable() - ) or (key_event.key == "paste" and bool(key_event.char)) + direct_text = (len(key_event.char) == 1 and key_event.char.isprintable()) or ( + key_event.key == "paste" and bool(key_event.char) + ) if ( self._type_to_edit_input and isinstance(focused_option, InputOption) diff --git a/src/iac_code/web/events.py b/src/iac_code/web/events.py index a51f4e13..368c9953 100644 --- a/src/iac_code/web/events.py +++ b/src/iac_code/web/events.py @@ -413,9 +413,7 @@ def translate_stream_event(self, event: StreamEvent, *, turn_id: str) -> dict[st summary=event.result, artifacts=[public_metadata] if public_metadata else [], submitted_delta=submitted_delta if isinstance(submitted_delta, dict) else None, - normalized_conclusion=( - normalized_conclusion if isinstance(normalized_conclusion, dict) else None - ), + normalized_conclusion=(normalized_conclusion if isinstance(normalized_conclusion, dict) else None), ) if isinstance(event, MCPProgressEvent): payload = mcp_progress_metadata(event) diff --git a/src/iac_code/web/pipeline_transcript.py b/src/iac_code/web/pipeline_transcript.py index 445ed2b6..05cc6c7c 100644 --- a/src/iac_code/web/pipeline_transcript.py +++ b/src/iac_code/web/pipeline_transcript.py @@ -1522,8 +1522,7 @@ def build_pipeline_transcript_rows(envelopes: Iterable[Mapping[str, Any]]) -> li # weave persisted ``source=pipeline`` replies right after their prompt rather # than appending them after the whole replay (Issue 2 misordering). anchors = translator.input_prompt_anchors or [ - {"messageId": message_id, "kind": "", "stepId": ""} - for message_id in translator.input_prompt_message_ids + {"messageId": message_id, "kind": "", "stepId": ""} for message_id in translator.input_prompt_message_ids ] for anchor in anchors: if anchor.get("expectsVisibleAnswer") == "0": diff --git a/tests/a2a/test_executor.py b/tests/a2a/test_executor.py index feb8eb8a..0ff2c028 100644 --- a/tests/a2a/test_executor.py +++ b/tests/a2a/test_executor.py @@ -2884,9 +2884,7 @@ async def test_executor_runs_normal_mode_when_iac_code_mode_is_normal( @pytest.mark.asyncio -async def test_normal_mode_ignores_stale_pipeline_name( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path -) -> None: +async def test_normal_mode_ignores_stale_pipeline_name(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: monkeypatch.setenv("IAC_CODE_MODE", "pipeline") loop = FakeAgentLoop([TextDeltaEvent(text="normal")]) runtime = FakeRuntime(agent_loop=loop, session_id="session-1") @@ -4294,9 +4292,7 @@ def test_region_only_metadata_copies_configured_credential_without_mutating_it( ) executor = self._make_executor() - result = executor._resolve_aliyun_credential( - {"iac_code": {"alibaba_cloud_region_id": "cn-beijing"}} - ) + result = executor._resolve_aliyun_credential({"iac_code": {"alibaba_cloud_region_id": "cn-beijing"}}) assert result is not None assert result is not configured @@ -4312,9 +4308,7 @@ def test_region_only_metadata_returns_none_without_configured_credential( monkeypatch.setattr("iac_code.a2a.executor.AliyunCredentials.load", lambda: None) executor = self._make_executor() - result = executor._resolve_aliyun_credential( - {"iac_code": {"alibaba_cloud_region_id": "cn-beijing"}} - ) + result = executor._resolve_aliyun_credential({"iac_code": {"alibaba_cloud_region_id": "cn-beijing"}}) assert result is None @@ -4322,9 +4316,7 @@ def test_region_only_metadata_rejects_invalid_region(self) -> None: executor = self._make_executor() with pytest.raises(InvalidParamsError, match="Unsupported Alibaba Cloud region ID"): - executor._resolve_aliyun_credential( - {"iac_code": {"alibaba_cloud_region_id": "https://example.com"}} - ) + executor._resolve_aliyun_credential({"iac_code": {"alibaba_cloud_region_id": "https://example.com"}}) @pytest.mark.asyncio @@ -4982,9 +4974,7 @@ async def test_persisted_permission_restores_backup_before_checkpoint_lookup( response=response, ) assert restored_storage.exists(cwd, session_id) - assert ( - restored_storage.session_dir(cwd, session_id) / "permission-waits" / f"{boundary_id}.json" - ).is_file() + assert (restored_storage.session_dir(cwd, session_id) / "permission-waits" / f"{boundary_id}.json").is_file() @pytest.mark.asyncio @@ -5306,8 +5296,7 @@ def register_cloud_tools(_registry, credentials, _services): index for index, event in enumerate(queue.events) if isinstance(event, TaskStatusUpdateEvent) - and dump(event).get("metadata", {}).get("iac_code", {}).get("inputReceived", {}).get("decision") - == "allow_once" + and dump(event).get("metadata", {}).get("iac_code", {}).get("inputReceived", {}).get("decision") == "allow_once" ] final_indices = [ index diff --git a/tests/a2a/test_pipeline_identity.py b/tests/a2a/test_pipeline_identity.py index 57760409..04454055 100644 --- a/tests/a2a/test_pipeline_identity.py +++ b/tests/a2a/test_pipeline_identity.py @@ -436,9 +436,7 @@ def test_two_sessions_run_different_pipelines_in_one_process( solution_executor = _inner_executor(pipeline_name=SELLING_SOLUTION_FIRST_PIPELINE_NAME) assert ( - legacy_executor._resolve_request_pipeline_name( - cwd=cwd, session_id=legacy_session, session_storage=storage - ) + legacy_executor._resolve_request_pipeline_name(cwd=cwd, session_id=legacy_session, session_storage=storage) == SELLING_PIPELINE_NAME ) assert ( @@ -584,7 +582,6 @@ def boom(*_args: Any, **_kwargs: Any) -> Any: assert snapshot_path.read_bytes() == snapshot_before assert not ctx.lock.locked() - @pytest.mark.asyncio @pytest.mark.parametrize("active_followup_only", [True, False], ids=["followup-probe", "full-request"]) async def test_a_mismatch_on_an_active_task_never_reaches_the_running_pipeline( @@ -729,11 +726,14 @@ def test_sidecar_identity_written_by_the_engine_is_readable_by_the_guard( raw = yaml.safe_load(meta_path.read_text(encoding="utf-8")) assert raw["pipeline_name"] == SELLING_SOLUTION_FIRST_PIPELINE_NAME - assert _inner_executor()._peek_durable_pipeline_name( - cwd=cwd, - session_id=session_id, - session_storage=storage, - ) == SELLING_SOLUTION_FIRST_PIPELINE_NAME + assert ( + _inner_executor()._peek_durable_pipeline_name( + cwd=cwd, + session_id=session_id, + session_storage=storage, + ) + == SELLING_SOLUTION_FIRST_PIPELINE_NAME + ) def test_concurrent_executors_resolve_independently(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/a2a/test_pipeline_snapshot.py b/tests/a2a/test_pipeline_snapshot.py index 24cbee66..da0a5781 100644 --- a/tests/a2a/test_pipeline_snapshot.py +++ b/tests/a2a/test_pipeline_snapshot.py @@ -939,9 +939,7 @@ def test_reduce_public_narrative_shape_survives_incremental_reduction() -> None: def test_reduce_legacy_message_keeps_marker_fallback_after_resume() -> None: existing = reduce_pipeline_events([]) - existing["display"]["messages"] = [ - {"scope": "step", "runId": "step-a-1", "round": 1, "text": "legacy public text"} - ] + existing["display"]["messages"] = [{"scope": "step", "runId": "step-a-1", "round": 1, "text": "legacy public text"}] resumed = reduce_pipeline_events( [ diff --git a/tests/a2a/test_pipeline_stream.py b/tests/a2a/test_pipeline_stream.py index c2b22f60..76e34b70 100644 --- a/tests/a2a/test_pipeline_stream.py +++ b/tests/a2a/test_pipeline_stream.py @@ -1850,9 +1850,7 @@ async def test_publish_file_backed_completion_artifact_keeps_body_only_in_artifa if dump(event).get("metadata", {}).get("iac_code", {}).get("pipeline", {}).get("eventType") ] artifact_status = next(event for event in status_events if event["eventType"] == "artifact_created") - journal_artifact = next( - event for event in publisher.journal.read_all() if event["eventType"] == "artifact_created" - ) + journal_artifact = next(event for event in publisher.journal.read_all() if event["eventType"] == "artifact_created") snapshot = publisher.snapshot_store.load() assert snapshot is not None snapshot_artifact = snapshot["display"]["artifacts"][0] @@ -1864,7 +1862,7 @@ async def test_publish_file_backed_completion_artifact_keeps_body_only_in_artifa assert snapshot_artifact["dedupeKey"] == artifact_status["artifact"]["dedupeKey"] for persisted in (artifact_status, journal_artifact, snapshot): assert template_body not in str(persisted) - assert "\"content\"" not in str(persisted) + assert '"content"' not in str(persisted) @pytest.mark.asyncio diff --git a/tests/a2a_e2e/test_start_chat_permission_wait_runner.py b/tests/a2a_e2e/test_start_chat_permission_wait_runner.py index 11dbd7fc..b2d08c2a 100644 --- a/tests/a2a_e2e/test_start_chat_permission_wait_runner.py +++ b/tests/a2a_e2e/test_start_chat_permission_wait_runner.py @@ -653,25 +653,25 @@ def test_real_runner_records_assistant_diagram_and_cloud_permission_block_order( stdout = "\n".join( json.dumps(item) for item in ( - { - "type": "assistant", - "message": { - "content": [ - {"type": "text", "text": "```mermaid\ngraph TD\n```"}, - { - "type": "tool_use", - "id": "bridge-1", - "name": "Bash", - "input": { - "command": ( - "ALICLOUD_ROS_AGENT_STATE_DIR=/tmp/state python3 " - "/repo/skills/alicloud-ros-agent/scripts/ros_agent.py " - "start --prompt-file request.txt --mode normal --follow" - ) + { + "type": "assistant", + "message": { + "content": [ + {"type": "text", "text": "```mermaid\ngraph TD\n```"}, + { + "type": "tool_use", + "id": "bridge-1", + "name": "Bash", + "input": { + "command": ( + "ALICLOUD_ROS_AGENT_STATE_DIR=/tmp/state python3 " + "/repo/skills/alicloud-ros-agent/scripts/ros_agent.py " + "start --prompt-file request.txt --mode normal --follow" + ) + }, }, - }, - ] - }, + ] + }, }, { "type": "user", diff --git a/tests/agui/test_app.py b/tests/agui/test_app.py index fad0388c..c88f73c7 100644 --- a/tests/agui/test_app.py +++ b/tests/agui/test_app.py @@ -657,9 +657,7 @@ class SyncingPreflightClient(FakeA2AClient): async def ensure_session_restored(self, _url, *, cwd, session_id, task_id=None): self.restored_sessions.append((cwd, session_id, task_id)) self.resume_preflight_calls.append("ensure_session_restored") - raise A2ASessionBackupNotReadyError( - "Session backup is still synchronizing. Retry after 3 seconds." - ) + raise A2ASessionBackupNotReadyError("Session backup is still synchronizing. Retry after 3 seconds.") fake = SyncingPreflightClient(interrupt=True) adapter = AguiA2AAdapter(a2a_url="http://a2a/", client=fake) @@ -976,10 +974,10 @@ async def test_question_selection_resume_is_sent_to_same_a2a_task(tmp_path, monk assert _events(first)[-1]["outcome"]["type"] == "interrupt" assert fake.resumed_prompts == [("Plan B", "task-1")] second_events = _events(second) - assert sum( - event.get("type") == "TOOL_CALL_RESULT" and event.get("toolCallId") == "ask-1" - for event in second_events - ) == 1 + assert ( + sum(event.get("type") == "TOOL_CALL_RESULT" and event.get("toolCallId") == "ask-1" for event in second_events) + == 1 + ) assert second_events[-1]["outcome"] == {"type": "success"} @@ -1175,9 +1173,7 @@ def test_mapper_consumes_real_local_a2a_wire_contract(tmp_path, monkeypatch) -> }, ) as response: raw_events = [ - json.loads(line.removeprefix("data: ")) - for line in response.iter_lines() - if line.startswith("data: ") + json.loads(line.removeprefix("data: ")) for line in response.iter_lines() if line.startswith("data: ") ] mapper = A2AEventMapper(thread_id="thread-1", run_id="run-1") diff --git a/tests/agui/test_http_sse_integration.py b/tests/agui/test_http_sse_integration.py index 8743b84a..dee911aa 100644 --- a/tests/agui/test_http_sse_integration.py +++ b/tests/agui/test_http_sse_integration.py @@ -220,9 +220,7 @@ async def record_cancel(self, context, event_queue): "TASK_STATE_CANCELED", } assert cancel_calls == [] - persisted = json.loads( - (state_dir / "threads" / "thread-http-sse.json").read_text(encoding="utf-8") - ) + persisted = json.loads((state_dir / "threads" / "thread-http-sse.json").read_text(encoding="utf-8")) assert persisted["execution"]["pending"][interrupt_id]["sideband"] is True second_agui = create_agui_app(a2a_url=a2a_url, state_dir=state_dir) diff --git a/tests/agui/test_persistence.py b/tests/agui/test_persistence.py index 6dd3925f..97b108c0 100644 --- a/tests/agui/test_persistence.py +++ b/tests/agui/test_persistence.py @@ -1154,13 +1154,16 @@ async def test_shared_state_dir_keeps_interleaved_threads_isolated(tmp_path: Pat second_payload["threadId"] = "thread-2" second_payload["forwardedProps"]["iacCode"]["rosInvocationId"] = "invocation-2" - async with httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=first_adapter)), - base_url="http://test", - ) as first_client, httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=second_adapter)), - base_url="http://test", - ) as second_client: + async with ( + httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=first_adapter)), + base_url="http://test", + ) as first_client, + httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=second_adapter)), + base_url="http://test", + ) as second_client, + ): first_response = await first_client.post("/", json=first_payload) second_response = await second_client.post("/", json=second_payload) @@ -1178,13 +1181,16 @@ async def test_shared_state_dir_keeps_interleaved_threads_isolated(tmp_path: Pat restarted_first = AguiA2AAdapter(a2a_url="http://a2a/", client=FakeA2AClient(), state_dir=state_dir) restarted_second = AguiA2AAdapter(a2a_url="http://a2a/", client=FakeA2AClient(), state_dir=state_dir) - async with httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=restarted_first)), - base_url="http://test", - ) as first_client, httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=restarted_second)), - base_url="http://test", - ) as second_client: + async with ( + httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=restarted_first)), + base_url="http://test", + ) as first_client, + httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=restarted_second)), + base_url="http://test", + ) as second_client, + ): first_replay = await first_client.post("/", json=first_payload) second_replay = await second_client.post("/", json=second_payload) @@ -1203,13 +1209,16 @@ async def test_corrupt_thread_state_does_not_block_another_thread(tmp_path: Path second_payload = _payload(tmp_path) second_payload["threadId"] = "thread-2" second_payload["forwardedProps"]["iacCode"]["rosInvocationId"] = "invocation-2" - async with httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=first_adapter)), - base_url="http://test", - ) as first_client, httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=second_adapter)), - base_url="http://test", - ) as second_client: + async with ( + httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=first_adapter)), + base_url="http://test", + ) as first_client, + httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=second_adapter)), + base_url="http://test", + ) as second_client, + ): await first_client.post("/", json=first_payload) await second_client.post("/", json=second_payload) diff --git a/tests/pipeline/engine/test_complete_step_tool.py b/tests/pipeline/engine/test_complete_step_tool.py index 827d4e26..629cf27e 100644 --- a/tests/pipeline/engine/test_complete_step_tool.py +++ b/tests/pipeline/engine/test_complete_step_tool.py @@ -335,9 +335,7 @@ async def test_authoritative_candidate_is_injected_into_both_handoff_fields(self authoritative_candidate_context_field="solution_selection.selected_candidate", authoritative_candidate_targets=("selected_candidate", "selected_candidate_result.candidate"), ), - completion_guard_state={ - "context_snapshot": {"solution_selection": {"selected_candidate": candidate}} - }, + completion_guard_state={"context_snapshot": {"solution_selection": {"selected_candidate": candidate}}}, ) result = await tool.execute( diff --git a/tests/pipeline/engine/test_hard_constraints.py b/tests/pipeline/engine/test_hard_constraints.py index 0ccf42c3..819b3ced 100644 --- a/tests/pipeline/engine/test_hard_constraints.py +++ b/tests/pipeline/engine/test_hard_constraints.py @@ -151,6 +151,8 @@ def test_validate_checks_accepts_llm_pass_when_code_verification_fails(): ) == [] ) + + def test_v2_accepts_llm_pass_when_tool_has_no_resolvable_evidence(): constraint = _constraint(verification_mode="tool") llm_passed = _check(constraint, status="satisfied", evidence=[]) diff --git a/tests/pipeline/engine/test_step_executor.py b/tests/pipeline/engine/test_step_executor.py index 8cd32123..84e6087d 100644 --- a/tests/pipeline/engine/test_step_executor.py +++ b/tests/pipeline/engine/test_step_executor.py @@ -3064,11 +3064,15 @@ def test_rich_candidate_resume_uses_compact_schema_and_preserves_first_conclusio surface="a2a_rich", ) - tool_schema = executor._build_step_tools( - step, - context, - compact_candidate_selection=True, - ).get("complete_step").input_schema + tool_schema = ( + executor._build_step_tools( + step, + context, + compact_candidate_selection=True, + ) + .get("complete_step") + .input_schema + ) conclusion_schema = tool_schema["properties"]["conclusion"] assert conclusion_schema["required"] == [ "selected_candidate_name", @@ -3151,11 +3155,15 @@ def test_stale_candidate_conclusion_cannot_enable_compact_resume_schema(self, tm context, resume_candidate_selection=True, ) - tool_schema = executor._build_step_tools( - step, - context, - compact_candidate_selection=preserved is not None, - ).get("complete_step").input_schema + tool_schema = ( + executor._build_step_tools( + step, + context, + compact_candidate_selection=preserved is not None, + ) + .get("complete_step") + .input_schema + ) assert preserved is None conclusion_schema = tool_schema["properties"]["conclusion"] diff --git a/tests/pipeline/selling/test_terminal_ui_contract.py b/tests/pipeline/selling/test_terminal_ui_contract.py index c4e2cd6a..ad9382b7 100644 --- a/tests/pipeline/selling/test_terminal_ui_contract.py +++ b/tests/pipeline/selling/test_terminal_ui_contract.py @@ -133,9 +133,7 @@ def test_confirm_prompt_tells_model_to_preserve_parameter_overrides(): def test_confirm_prompts_share_selection_contract_structure(): repl_prompt = (_selling_pipeline_dir() / "prompts" / "confirm_and_select.md").read_text(encoding="utf-8") a2a_prompt = (_selling_pipeline_dir() / "prompts" / "confirm_and_select.a2a.md").read_text(encoding="utf-8") - rich_prompt = (_selling_pipeline_dir() / "prompts" / "confirm_and_select.a2a.rich.md").read_text( - encoding="utf-8" - ) + rich_prompt = (_selling_pipeline_dir() / "prompts" / "confirm_and_select.a2a.rich.md").read_text(encoding="utf-8") shared_fragments = [ "## 首次执行", diff --git a/tests/pipeline/selling_solution_first/test_completion_projection.py b/tests/pipeline/selling_solution_first/test_completion_projection.py index 561f65c5..a0b312ea 100644 --- a/tests/pipeline/selling_solution_first/test_completion_projection.py +++ b/tests/pipeline/selling_solution_first/test_completion_projection.py @@ -71,9 +71,7 @@ def _step_config(step) -> StepConfig: completion_record_contract=step.config.get("completion_record_contract"), hard_constraint_evidence_contract=step.config.get("hard_constraint_evidence_contract"), completion_context_paths=tuple(step.config.get("completion_context_paths", [])), - confirmation_accepts_parameter_overrides=( - step.config.get("confirmation_accepts_parameter_overrides") is True - ), + confirmation_accepts_parameter_overrides=(step.config.get("confirmation_accepts_parameter_overrides") is True), ) @@ -503,10 +501,7 @@ def test_model_schema_keeps_nested_structure_without_annotations_or_runtime_fiel def test_final_model_tool_schemas_stay_within_measured_token_budgets(self, loaded): counter = TokenCounter(model="deepseek-v4-flash-0731") - counts = { - step.step_id: counter.count_tool_definition(_tool(step)) - for step in loaded.steps - } + counts = {step.step_id: counter.count_tool_definition(_tool(step)) for step in loaded.steps} # Step 1 complete_step 只提交步骤语义;候选详情由展示工具记录承载。 assert counts["solution_planning_and_selection"] <= 400 @@ -572,8 +567,7 @@ def test_raw_error_returns_at_most_five_field_diagnostics(self): "type": "object", "required": field_names, "properties": { - name: {"type": "string", "description": f"Description for {name}."} - for name in field_names + name: {"type": "string", "description": f"Description for {name}."} for name in field_names }, "additionalProperties": False, }, @@ -900,9 +894,10 @@ def test_persuasion_entries_are_trimmed_and_survive_reopen_as_flat_fields(self, {"conclusion": {"status": "awaiting_selection"}}, ) assert reopened.conclusion["candidates"][0]["why_recommended"] == ["用户点名要托管数据库"] - assert reopened.conclusion["candidates"][0]["problems_solved"] == awaiting.conclusion["candidates"][0][ - "problems_solved" - ] + assert ( + reopened.conclusion["candidates"][0]["problems_solved"] + == awaiting.conclusion["candidates"][0]["problems_solved"] + ) @pytest.mark.asyncio async def test_execute_preserves_submitted_delta_and_returns_normalized_result(self, loaded): @@ -1448,9 +1443,7 @@ def test_ros_resource_mapping_is_normalized_to_monthly_total_and_details(self, l cost = result.conclusion["selected_candidate_result"]["cost"] assert cost["quote_status"] == "succeeded" - assert cost["monthly_estimate"] == ( - "¥360.00/month (list price; about ¥88.80/month after contract discount)" - ) + assert cost["monthly_estimate"] == ("¥360.00/month (list price; about ¥88.80/month after contract discount)") assert cost["resources"] == [ { "type": "Instance", @@ -1497,9 +1490,7 @@ def test_ros_subscription_period_total_is_normalized_to_monthly_price(self, load cost = result.conclusion["selected_candidate_result"]["cost"] assert cost["quote_status"] == "succeeded" - assert cost["monthly_estimate"] == ( - "¥630.00/month (list price; about ¥208.84/month after contract discount)" - ) + assert cost["monthly_estimate"] == ("¥630.00/month (list price; about ¥208.84/month after contract discount)") assert cost["resources"] == [ { "type": "DBInstance", @@ -1617,9 +1608,7 @@ def test_template_evidence_uses_ros_aware_yaml_and_internal_dotted_path(self, lo _write_template_with_intrinsic(tmp_path) step = _step(loaded, "materialize_selected_candidate") check = _check() - check["evidence"] = [ - {"type": "template", "template_path": "Parameters.DBInstanceStorage.Default"} - ] + check["evidence"] = [{"type": "template", "template_path": "Parameters.DBInstanceStorage.Default"}] result = _finalize( _tool( @@ -1630,9 +1619,7 @@ def test_template_evidence_uses_ros_aware_yaml_and_internal_dotted_path(self, lo ), _waiting_delta(check=check), ) - evidence = result.conclusion["selected_candidate_result"]["cost"]["hard_constraint_checks"][0][ - "evidence" - ][0] + evidence = result.conclusion["selected_candidate_result"]["cost"]["hard_constraint_checks"][0]["evidence"][0] assert evidence["actual_value"] == 120 assert evidence["template_path"] == "Parameters.DBInstanceStorage.Default" @@ -1651,9 +1638,7 @@ def test_context_evidence_is_allowlisted_and_resolved_by_python(self, loaded, tm ), _waiting_delta(check=_check(evidence_type="context")), ) - evidence = result.conclusion["selected_candidate_result"]["cost"]["hard_constraint_checks"][0][ - "evidence" - ][0] + evidence = result.conclusion["selected_candidate_result"]["cost"]["hard_constraint_checks"][0]["evidence"][0] assert evidence == { "type": "context", diff --git a/tests/pipeline/selling_solution_first/test_materialize_step.py b/tests/pipeline/selling_solution_first/test_materialize_step.py index a90b35fa..41200414 100644 --- a/tests/pipeline/selling_solution_first/test_materialize_step.py +++ b/tests/pipeline/selling_solution_first/test_materialize_step.py @@ -143,9 +143,9 @@ def prompt_text() -> str: @pytest.fixture(scope="module") def skill_text() -> str: - return ( - _pipeline_dir() / "skills" / "iac-aliyun-materialize-selected-candidate" / "SKILL.md" - ).read_text(encoding="utf-8") + return (_pipeline_dir() / "skills" / "iac-aliyun-materialize-selected-candidate" / "SKILL.md").read_text( + encoding="utf-8" + ) def _conclusion(**overrides): @@ -293,9 +293,7 @@ def test_natural_language_confirmation_accepts_only_the_incremental_branch(self, assert tool.validate_completion_input(tool_input) is None assert "selected_candidate" not in tool_input["conclusion"] assert "candidate" not in tool_input["conclusion"]["selected_candidate_result"] - assert tool_input["conclusion"]["selected_candidate_result"]["cost"]["monthly_estimate"].startswith( - "¥1,280" - ) + assert tool_input["conclusion"]["selected_candidate_result"]["cost"]["monthly_estimate"].startswith("¥1,280") def test_confirmation_cannot_skip_the_dedicated_waiting_state(self, step, tmp_path): state = _happy_guard_state(str(tmp_path)) @@ -419,7 +417,7 @@ def test_structured_confirm_with_empty_overrides_preserves_current_overrides(sel "input_type": "structured", "user_input": user_message, "parameter_overrides": {"ZoneId": "cn-hangzhou-h"}, - } + }, ) tool = _tool(step, state, user_message=user_message) @@ -826,7 +824,7 @@ def test_dedicated_confirmation_supports_structured_and_natural_language_input(s def test_parameter_adjustment_reprices_and_rewrites_the_solution_summary(self, skill_text): assert "重新执行必要的参数约束查询、PreviewStack 和 ROS 精确询价" in skill_text assert "重新生成 `solution_summary`" in skill_text - assert "再次提交 `status: \"awaiting_confirmation\"`" in skill_text + assert '再次提交 `status: "awaiting_confirmation"`' in skill_text def test_free_text_distinguishes_parameter_architecture_and_new_intent_changes(self, skill_text): assert "调整当前参数" in skill_text diff --git a/tests/pipeline/selling_solution_first/test_pipeline_definition.py b/tests/pipeline/selling_solution_first/test_pipeline_definition.py index 906eb044..57cb74ae 100644 --- a/tests/pipeline/selling_solution_first/test_pipeline_definition.py +++ b/tests/pipeline/selling_solution_first/test_pipeline_definition.py @@ -138,9 +138,7 @@ def test_candidate_and_deployment_options_keep_separate_schemas(self, raw_yaml): confirmation_options = raw_yaml["steps"][1]["conclusion_schema"]["properties"]["options"] assert confirmation_options["minItems"] == 2 assert confirmation_options["maxItems"] == 4 - required_actions = { - rule["contains"]["properties"]["action"]["const"] for rule in confirmation_options["allOf"] - } + required_actions = {rule["contains"]["properties"]["action"]["const"] for rule in confirmation_options["allOf"]} assert required_actions == {"confirm", "cancel"} assert set(confirmation_options["items"]["properties"]["action"]["enum"]) == { "confirm", @@ -161,9 +159,10 @@ def test_complete_step_schemas_describe_branch_identity_and_handoff_fields(self, candidate = planning_schema["properties"]["candidates"]["items"] assert "原样取自" in candidate["description"] assert "Step 2 唯一允许写入" in candidate["properties"]["output_path"]["description"] - assert "0 基下标" in planning_schema["properties"]["options"]["items"]["properties"][ - "candidate_index" - ]["description"] + assert ( + "0 基下标" + in planning_schema["properties"]["options"]["items"]["properties"]["candidate_index"]["description"] + ) assert "原样等于" in planning_schema["properties"]["selected_candidate"]["description"] assert materialize_schema is not None @@ -173,9 +172,7 @@ def test_complete_step_schemas_describe_branch_identity_and_handoff_fields(self, assert "面向最终用户" in materialized["solution_summary"]["description"] assert "ROS 精确询价" in materialized["cost"]["description"] assert "同一路径" in materialized["template"]["properties"]["file_path"]["description"] - assert "没有覆盖时必须使用空对象" in materialize_schema["properties"]["parameter_overrides"][ - "description" - ] + assert "没有覆盖时必须使用空对象" in materialize_schema["properties"]["parameter_overrides"]["description"] assert "真实用户确认输入" in materialize_schema["properties"]["confirmation"]["description"] # Step 3 继续从共享 deploying skill 继承 schema,不在新 pipeline 复制一份。 diff --git a/tests/pipeline/selling_solution_first/test_solution_planning_step.py b/tests/pipeline/selling_solution_first/test_solution_planning_step.py index d40534d1..5479f037 100644 --- a/tests/pipeline/selling_solution_first/test_solution_planning_step.py +++ b/tests/pipeline/selling_solution_first/test_solution_planning_step.py @@ -334,9 +334,7 @@ async def test_step_two_emits_a_dedicated_confirmation_payload_and_waits_again_a first_resume = await _drain(runner.resume(encode_selected_candidate(CANDIDATES[1]["name"], 1))) confirmation_wait = next( - event - for event in _input_required(first_resume) - if event.step_id == "materialize_selected_candidate" + event for event in _input_required(first_resume) if event.step_id == "materialize_selected_candidate" ) assert confirmation_wait.data["kind"] == "deployment_confirmation" assert confirmation_wait.data["solution_summary"] == "SLB + 双 ECS + RDS 高可用方案" @@ -379,11 +377,7 @@ async def test_unchanged_structured_confirm_is_resolved_once_and_advances_to_dep events = await _drain(runner.resume('{"action":"confirm","parameter_overrides":{}}')) - assert not [ - event - for event in _input_required(events) - if event.step_id == "materialize_selected_candidate" - ] + assert not [event for event in _input_required(events) if event.step_id == "materialize_selected_candidate"] assert [call["step_id"] for call in executor.calls] == [ STEP_ID, STEP_ID, @@ -692,9 +686,7 @@ def prompt_text(self) -> str: @pytest.fixture(scope="class") def skill_text(self) -> str: - return ( - _pipeline_dir() / "skills" / "iac-aliyun-solution-first" / "SKILL.md" - ).read_text(encoding="utf-8") + return (_pipeline_dir() / "skills" / "iac-aliyun-solution-first" / "SKILL.md").read_text(encoding="utf-8") def test_conclusion_schema_covers_the_three_outcomes(self, raw_step): schema = raw_step["conclusion_schema"] @@ -759,9 +751,15 @@ def test_compact_completion_requires_authoritative_resource_lifecycle(self, raw_ assert set(intent["required"]) == {"resource_intents", "hard_constraints"} assert resource_intents["minItems"] == 1 - assert all(action in resource_intents["description"] for action in ( - "create", "use_existing", "reference", "forbid", - )) + assert all( + action in resource_intents["description"] + for action in ( + "create", + "use_existing", + "reference", + "forbid", + ) + ) assert "ECS:forbid" in resource_intents["description"] def test_options_require_the_candidate_index_coordinate(self, raw_step): diff --git a/tests/pipeline_e2e/test_selling_solution_first_run_scenarios.py b/tests/pipeline_e2e/test_selling_solution_first_run_scenarios.py index a71fe521..04d3588f 100644 --- a/tests/pipeline_e2e/test_selling_solution_first_run_scenarios.py +++ b/tests/pipeline_e2e/test_selling_solution_first_run_scenarios.py @@ -600,9 +600,7 @@ def _json_request(_base_url: str, _method: str, _path: str) -> dict[str, object] def test_web_confirmation_boundary_accepts_repeated_parameter_questions(runner: ModuleType) -> None: for kind in ("ask_user_question", "deployment_confirmation"): - assert runner._web_at_confirmation_boundary( - {"pipeline": {"snapshot": {"pendingInput": {"kind": kind}}}} - ) + assert runner._web_at_confirmation_boundary({"pipeline": {"snapshot": {"pendingInput": {"kind": kind}}}}) assert not runner._web_at_confirmation_boundary( {"pipeline": {"snapshot": {"pendingInput": {"kind": "candidate_selection"}}}} @@ -611,9 +609,7 @@ def test_web_confirmation_boundary_accepts_repeated_parameter_questions(runner: def test_web_materialize_boundary_fails_fast_on_unexpected_rollback(runner: ModuleType) -> None: for kind in ("ask_user_question", "deployment_confirmation", "candidate_selection", "candidate_select"): - assert runner._web_at_materialize_boundary( - {"pipeline": {"snapshot": {"pendingInput": {"kind": kind}}}} - ) + assert runner._web_at_materialize_boundary({"pipeline": {"snapshot": {"pendingInput": {"kind": kind}}}}) def test_w02_parameter_answer_preserves_create_goal(runner: ModuleType) -> None: @@ -805,9 +801,7 @@ def test_web_session_uses_valid_unattended_permission_mode(runner: ModuleType, t assert payload["mode"] == "pipeline" -def test_browser_dependency_preflight_reports_missing_node( - runner: ModuleType, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_browser_dependency_preflight_reports_missing_node(runner: ModuleType, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(runner.shutil, "which", lambda _name: None) result = runner._run_browser_dependency_preflight(timeout=1.0) @@ -1517,9 +1511,7 @@ def test_desktop_result_requires_the_full_native_contract(runner: ModuleType) -> assert not all(runner.validate_desktop_result(result).values()) -def test_desktop_source_resource_audit_follows_linked_reference_directory( - runner: ModuleType, tmp_path: Path -) -> None: +def test_desktop_source_resource_audit_follows_linked_reference_directory(runner: ModuleType, tmp_path: Path) -> None: source_root = tmp_path / "pipeline" shared_references = tmp_path / "shared-references" linked_references = source_root / "skills" / "materialize" / "references" diff --git a/tests/providers/test_dashscope_provider.py b/tests/providers/test_dashscope_provider.py index 41f6ebea..ede49bf7 100644 --- a/tests/providers/test_dashscope_provider.py +++ b/tests/providers/test_dashscope_provider.py @@ -103,15 +103,11 @@ def test_kimi_k3_preserves_thinking_without_enable_flag(self): def test_bailian_hosted_kimi_k3_keeps_always_on_thinking(self): p = DashScopeProvider(model="kimi-k3", api_key="k", thinking_enabled=False) - assert p._build_thinking_kwargs() == { - "extra_body": {"enable_thinking": True, "preserve_thinking": True} - } + assert p._build_thinking_kwargs() == {"extra_body": {"enable_thinking": True, "preserve_thinking": True}} def test_qwen38_open_model_supports_thinking_budget(self): p = DashScopeProvider(model="qwen3.8-2.4t-a95b", api_key="k", thinking_budget=2048) - assert p._build_thinking_kwargs() == { - "extra_body": {"enable_thinking": True, "thinking_budget": 2048} - } + assert p._build_thinking_kwargs() == {"extra_body": {"enable_thinking": True, "thinking_budget": 2048}} def test_stepfun_uses_its_documented_effort_values(self): p = DashScopeProvider(model="stepfun/step-3.7-flash", api_key="k", effort="medium") diff --git a/tests/providers/test_provider_model_research_updates.py b/tests/providers/test_provider_model_research_updates.py index 25e96608..7c6968f7 100644 --- a/tests/providers/test_provider_model_research_updates.py +++ b/tests/providers/test_provider_model_research_updates.py @@ -443,9 +443,7 @@ def test_dashscope_new_model_protocols_are_not_flattened() -> None: model="kimi-k3", api_key="k", thinking_enabled=False, - )._build_thinking_kwargs() == { - "extra_body": {"enable_thinking": True, "preserve_thinking": True} - } + )._build_thinking_kwargs() == {"extra_body": {"enable_thinking": True, "preserve_thinking": True}} stepfun = get_thinking_spec("dashscope", "stepfun/step-3.7-flash") assert stepfun.allowed_efforts == (EffortLevel.LOW, EffortLevel.MEDIUM, EffortLevel.HIGH) diff --git a/tests/providers/test_qwen_provider.py b/tests/providers/test_qwen_provider.py index f899b43e..7f9fe358 100644 --- a/tests/providers/test_qwen_provider.py +++ b/tests/providers/test_qwen_provider.py @@ -770,9 +770,7 @@ async def test_terminal_attribution_separates_logical_wire_service_adapter_and_m provider_key_override="openai_compatible", ) events = [event async for event in manager.stream([Message.user("hi")], "base")] - attribution = next( - event.usage_attribution for event in events if isinstance(event, MessageEndEvent) - ) + attribution = next(event.usage_attribution for event in events if isinstance(event, MessageEndEvent)) assert ( attribution.logical_provider_key, attribution.wire_provider_key, @@ -905,9 +903,7 @@ async def test_unsafe_stream_successful_replay_keeps_terminal_attribution(self, @pytest.mark.asyncio class TestQwenResponses: async def test_streaming_and_non_streaming_cache_marker_scope(self): - stream_client = FakeOpenAIClient( - stream_chunks=[_chunk(content="ok"), _chunk(finish_reason="stop")] - ) + stream_client = FakeOpenAIClient(stream_chunks=[_chunk(content="ok"), _chunk(finish_reason="stop")]) stream_provider = QwenProvider(model="qwen3.7-plus", client=stream_client) _ = [ event @@ -942,14 +938,8 @@ async def test_streaming_and_non_streaming_cache_marker_scope(self): @pytest.mark.parametrize("streaming", [False, True]) async def test_required_thinking_error_rebuilds_once_and_is_learned(self, streaming): - error = _RequiredThinkingError( - "The value of the enable_thinking parameter is restricted to True." - ) - success = ( - [_chunk(content="ok"), _chunk(finish_reason="stop")] - if streaming - else _response(content="ok") - ) + error = _RequiredThinkingError("The value of the enable_thinking parameter is restricted to True.") + success = [_chunk(content="ok"), _chunk(finish_reason="stop")] if streaming else _response(content="ok") client, completions = _sequential_client(error, success, success) provider = QwenProvider( model="qwen3.8-max", @@ -974,9 +964,7 @@ async def test_required_thinking_error_rebuilds_once_and_is_learned(self, stream assert completions.calls[2]["extra_body"]["enable_thinking"] is True async def test_required_thinking_retry_reports_both_real_api_attempts(self, monkeypatch): - error = _RequiredThinkingError( - "The value of the enable_thinking parameter is restricted to True." - ) + error = _RequiredThinkingError("The value of the enable_thinking parameter is restricted to True.") client, completions = _sequential_client( error, [_chunk(content="ok"), _chunk(finish_reason="stop")], @@ -1031,9 +1019,7 @@ async def test_required_thinking_retry_reports_both_real_api_attempts(self, monk assert all(attrs[IacCodeAttr.PROVIDER_ADAPTER] == "qwen" for attrs in request_metrics) async def test_mandatory_learning_does_not_rewrite_an_existing_stream_context(self): - error = _RequiredThinkingError( - "The value of the enable_thinking parameter is restricted to True." - ) + error = _RequiredThinkingError("The value of the enable_thinking parameter is restricted to True.") client, completions = _sequential_client( error, _response(content="learned"), @@ -1133,16 +1119,12 @@ async def test_reasoning_content_wins_once_and_empty_blocks_fallback(self): client = FakeOpenAIClient( create_response=_response(content="answer", reasoning_content="", reasoning="must-not-appear") ) - response = await QwenProvider(model="qwen3.7-plus", client=client).complete( - [Message.user("hi")], "sys" - ) + response = await QwenProvider(model="qwen3.7-plus", client=client).complete([Message.user("hi")], "sys") assert response.thinking == "" assert response.thinking_blocks == [] async def test_unclosed_thinking_tag_is_unsafe(self): - client = FakeOpenAIClient( - stream_chunks=[_chunk(content="secret"), _chunk(finish_reason="stop")] - ) + client = FakeOpenAIClient(stream_chunks=[_chunk(content="secret"), _chunk(finish_reason="stop")]) provider = QwenProvider(model="qwen3.7-plus", client=client) with pytest.raises(UnsafeStreamProtocolError): _ = [event async for event in provider.stream([Message.user("hi")], "sys")] @@ -1151,9 +1133,7 @@ async def test_user_visible_stream_protocol_error_uses_runtime_translation(self, import iac_code.providers.streaming as streaming monkeypatch.setattr(streaming, "_", lambda message: f"translated:{message}") - client = FakeOpenAIClient( - stream_chunks=[_chunk(content="secret"), _chunk(finish_reason="stop")] - ) + client = FakeOpenAIClient(stream_chunks=[_chunk(content="secret"), _chunk(finish_reason="stop")]) provider = QwenProvider(model="qwen3.7-plus", client=client) with pytest.raises(UnsafeStreamProtocolError, match=r"^translated:Qwen emitted"): _ = [event async for event in provider.stream([Message.user("hi")], "sys")] @@ -1164,9 +1144,7 @@ async def test_tag_probe_literal_conflict_and_balanced_sequences(self): ) events = [ event - async for event in QwenProvider(model="qwen3.7-plus", client=literal).stream( - [Message.user("hi")], "sys" - ) + async for event in QwenProvider(model="qwen3.7-plus", client=literal).stream([Message.user("hi")], "sys") ] assert "".join(event.text for event in events if isinstance(event, TextDeltaEvent)) == "a.py' - "" + 'a.py' ) fallback = FakeOpenAIClient(stream_chunks=[_chunk(content=xml), _chunk(finish_reason="stop")]) provider = QwenProvider(model="qwen3.7-plus", client=fallback) @@ -1277,9 +1250,7 @@ async def test_strict_native_tool_call_and_xml_fallback(self): async def test_real_dashscope_empty_tool_delimiters_do_not_create_anonymous_call(self): client = FakeOpenAIClient( stream_chunks=[ - _chunk( - tool_calls=[ns(index=0, id="call_1", function=ns(name="read_file", arguments=""))] - ), + _chunk(tool_calls=[ns(index=0, id="call_1", function=ns(name="read_file", arguments=""))]), _chunk(tool_calls=[ns(index=0, id="", function=ns(name=None, arguments=""))]), _chunk(tool_calls=[ns(index=0, id="", function=ns(name=None, arguments='{"path": '))]), _chunk(tool_calls=[ns(index=0, id="", function=ns(name=None, arguments='"a.py"}'))]), @@ -1293,9 +1264,7 @@ async def test_real_dashscope_empty_tool_delimiters_do_not_create_anonymous_call [Message.user("hi")], "sys", [_tool()] ) ] - assert [event.input for event in events if isinstance(event, ToolUseEndEvent)] == [ - {"path": "a.py"} - ] + assert [event.input for event in events if isinstance(event, ToolUseEndEvent)] == [{"path": "a.py"}] assert next(event for event in events if isinstance(event, MessageEndEvent)).stop_reason == "tool_use" async def test_malformed_native_call_does_not_fall_through_to_xml(self): @@ -1332,9 +1301,7 @@ async def test_non_streaming_reasoning_and_xml(self): ], ) async def test_markdown_quoted_xml_is_never_executed(self, quoted): - stream_client = FakeOpenAIClient( - stream_chunks=[_chunk(content=quoted), _chunk(finish_reason="stop")] - ) + stream_client = FakeOpenAIClient(stream_chunks=[_chunk(content=quoted), _chunk(finish_reason="stop")]) events = [ event async for event in QwenProvider(model="qwen3.7-plus", client=stream_client).stream( @@ -1391,9 +1358,7 @@ async def test_native_tool_call_wins_over_xml_and_missing_finish_does_not_recove ) ] assert "".join(event.text for event in events if isinstance(event, TextDeltaEvent)) == xml - assert [event.input for event in events if isinstance(event, ToolUseEndEvent)] == [ - {"path": "native.py"} - ] + assert [event.input for event in events if isinstance(event, ToolUseEndEvent)] == [{"path": "native.py"}] no_finish = FakeOpenAIClient(stream_chunks=[_chunk(content=xml)]) events = [ diff --git a/tests/providers/test_qwen_tool_call_parser.py b/tests/providers/test_qwen_tool_call_parser.py index 3dc0de36..7427b3a7 100644 --- a/tests/providers/test_qwen_tool_call_parser.py +++ b/tests/providers/test_qwen_tool_call_parser.py @@ -137,8 +137,7 @@ def test_xml_requires_registered_parameterized_invoke_and_intent_guard(): def test_xml_rejects_duplicate_or_nested_parameters_and_preserves_scalar_strings(): duplicate = ( - 'a' - 'b' + 'ab' ) nested = 'a' assert recover_xml_tool_calls(duplicate, _tools()) is None @@ -157,14 +156,13 @@ def test_xml_multiple_calls_wrapper_entities_newlines_and_unicode(): recovery = recover_xml_tool_calls(text, tools) assert recovery.remaining_text == "" assert recovery.calls[0]["input"] == {"path": "目录/a.py"} - assert recovery.calls[1]["input"] == {"content": '&"\'', "meta": {"ok": True}} + assert recovery.calls[1]["input"] == {"content": "&\"'", "meta": {"ok": True}} @pytest.mark.parametrize( "text", [ - 'texta' - "", + 'texta', 'a', 'a', '~~~xml\na\n~~~', diff --git a/tests/skill_bridge/test_alicloud_ros_agent_bridge.py b/tests/skill_bridge/test_alicloud_ros_agent_bridge.py index 9b7c778e..a1b9b42e 100644 --- a/tests/skill_bridge/test_alicloud_ros_agent_bridge.py +++ b/tests/skill_bridge/test_alicloud_ros_agent_bridge.py @@ -1158,7 +1158,7 @@ def test_cli_plugin_parser_accepts_pretty_printed_objects_and_arrays() -> None: def test_cli_plugin_parser_reports_an_unterminated_buffer_as_malformed() -> None: - events = list(bridge.iter_cli_plugin_payloads(['{\n', ' "data": {\n'])) + events = list(bridge.iter_cli_plugin_payloads(["{\n", ' "data": {\n'])) assert events == [(None, '{\n "data": {')] diff --git a/tests/skill_bridge/test_runtime_release.py b/tests/skill_bridge/test_runtime_release.py index 9ad3da97..27a9efd4 100644 --- a/tests/skill_bridge/test_runtime_release.py +++ b/tests/skill_bridge/test_runtime_release.py @@ -107,9 +107,7 @@ def test_runtime_archive_and_version_marker_are_rooted_consistently(tmp_path: Pa assert "artifactRevision" not in marker -def test_runtime_a2a_smoke_checks_health_and_agent_card( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_runtime_a2a_smoke_checks_health_and_agent_card(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: module = _load_module("skill_runtime_smoke", BUILD_SCRIPT) server = tmp_path / "server.py" server.write_text( diff --git a/tests/tools/cloud/aliyun/test_ros_validate_hook.py b/tests/tools/cloud/aliyun/test_ros_validate_hook.py index e0744b8e..259f1190 100644 --- a/tests/tools/cloud/aliyun/test_ros_validate_hook.py +++ b/tests/tools/cloud/aliyun/test_ros_validate_hook.py @@ -446,9 +446,7 @@ def test_eip_bound_instance_group_detects_select_getatt_reference(self) -> None: "Type": "ALIYUN::VPC::EIPAssociation", "Properties": { "AllocationId": {"Ref": "Eip"}, - "InstanceId": { - "Fn::Select": [0, {"Fn::GetAtt": ["InstanceGroup", "InstanceIds"]}] - }, + "InstanceId": {"Fn::Select": [0, {"Fn::GetAtt": ["InstanceGroup", "InstanceIds"]}]}, }, }, }, diff --git a/tests/web/test_diagrams.py b/tests/web/test_diagrams.py index edd0a5ea..297f8f5d 100644 --- a/tests/web/test_diagrams.py +++ b/tests/web/test_diagrams.py @@ -255,9 +255,7 @@ def test_outputs_payload_includes_solution_first_step1_plan_and_rough_cost(tmp_p assert item["diagramStage"] == "optimized" assert item["optimized"] is True assert item["totalMonthlyCost"] == "约 ¥300/月(架构粗估)" - assert item["costItems"] == [ - {"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthly_cost": "约 ¥300/月"} - ] + assert item["costItems"] == [{"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthly_cost": "约 ¥300/月"}] assert item["views"] == [ { "id": "overview", @@ -292,9 +290,7 @@ def test_diagram_items_keeps_latest_step1_plan_and_step2_template(tmp_path): def test_diagram_items_ignores_unmarked_template_less_diagram(tmp_path): - manager = _Manager( - [_architecture_plan_envelope(0, "非规划图", "flowchart TD\n A", marked=False)] - ) + manager = _Manager([_architecture_plan_envelope(0, "非规划图", "flowchart TD\n A", marked=False)]) assert diagram_items(manager, _session(tmp_path)) == [] @@ -326,9 +322,7 @@ def test_solution_first_materialized_diagram_uses_exact_quote_and_optimized_cach assert item["mermaidSource"] == "graph TD\n OPT[优化图]" assert item["views"][0]["title"] == "优化总览" assert item["totalMonthlyCost"] == "¥88/月" - assert item["costItems"] == [ - {"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthly_cost": "¥88/月"} - ] + assert item["costItems"] == [{"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthly_cost": "¥88/月"}] def test_diagram_items_dedupes_solution_first_absolute_and_relative_template_paths(tmp_path): diff --git a/tests/web/test_frontend_static.py b/tests/web/test_frontend_static.py index 4a8cc77c..b7e3e3a6 100644 --- a/tests/web/test_frontend_static.py +++ b/tests/web/test_frontend_static.py @@ -2559,7 +2559,7 @@ def test_completed_turn_collapses_process_into_summary() -> None: # 「已处理」组的展开态必须跨重建保留:openKey 让 toggle 记录器登记用户操作、 # applyDetailsOpenOverrides 在重建后恢复;键取 turnId,缺 turnId 时回退首条消息 id。 assert 'const turnKey = turnId || text(agentMessages[0]?.messageId || agentMessages[0]?.id || "");' in app_source - assert 'details.dataset.openKey = `turnproc:${turnKey}`;' in app_source + assert "details.dataset.openKey = `turnproc:${turnKey}`;" in app_source # 只有最后一次工具调用之后的文本才是「最终回答」;此前每个步骤的文本旁白 # (夹在工具调用之间的 text delta)连同思考、工具一起折进「已处理」,不平铺成答案。 diff --git a/tests/web/test_pipeline_transcript.py b/tests/web/test_pipeline_transcript.py index a610783c..768c0ead 100644 --- a/tests/web/test_pipeline_transcript.py +++ b/tests/web/test_pipeline_transcript.py @@ -1119,8 +1119,7 @@ def test_repeated_step_completions_accumulate_processing_duration(): completed_markers = [ event for event in events - if event["type"] == PIPELINE_MARKER_EVENT - and event["payload"]["pipelineStep"]["status"] == "completed" + if event["type"] == PIPELINE_MARKER_EVENT and event["payload"]["pipelineStep"]["status"] == "completed" ] assert completed_markers[-1]["payload"]["pipelineStep"]["durationS"] == 135.44 diff --git a/tests/web/test_session_manager.py b/tests/web/test_session_manager.py index b953e252..68ced3c2 100644 --- a/tests/web/test_session_manager.py +++ b/tests/web/test_session_manager.py @@ -114,9 +114,7 @@ def test_runtime_settings_payload_redacts_only_editable_cloud_credentials(monkey ("cancel", "Cancel"), ), ) -def test_solution_first_pipeline_user_display_text_hides_structured_control_json( - action: str, expected: str -) -> None: +def test_solution_first_pipeline_user_display_text_hides_structured_control_json(action: str, expected: str) -> None: raw = json.dumps({"action": action, "parameter_overrides": {"ZoneId": "cn-hangzhou-i"}}) assert solution_first_pipeline_user_display_text("selling_solution_first", raw) == expected From 1579a4004cb5de454a66a5267ac8a6347b459390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=82=E9=A9=AC?= Date: Thu, 3 Sep 2026 18:08:21 +0800 Subject: [PATCH 2/2] chore: keep release diff within desktop scope --- src/iac_code/a2a/events.py | 6 +- src/iac_code/a2a/executor.py | 4 +- src/iac_code/a2a/pipeline_events.py | 3 +- src/iac_code/agent/agent_loop.py | 14 +++- src/iac_code/agui/adapter.py | 6 +- src/iac_code/agui/state.py | 5 +- src/iac_code/pipeline/constants.py | 4 +- .../pipeline/engine/complete_step_tool.py | 31 ++++++--- src/iac_code/pipeline/engine/loader.py | 11 ++- .../pipeline/engine/pipeline_runner.py | 4 +- .../pipeline/engine/resume_recovery.py | 6 +- src/iac_code/pipeline/engine/step_executor.py | 12 +++- .../selling_solution_first/hooks/deploying.py | 4 +- .../hooks/materialize_selected_candidate.py | 29 ++++---- .../hooks/solution_planning_and_selection.py | 6 +- .../tools/candidate_planning_records.py | 10 ++- .../tools/show_architecture_plan_tool.py | 4 +- .../tools/show_candidate_detail_tool.py | 8 ++- src/iac_code/providers/dashscope_endpoints.py | 8 ++- src/iac_code/providers/manager.py | 9 ++- src/iac_code/providers/openai_provider.py | 4 +- src/iac_code/providers/qwen_prompts.py | 4 +- src/iac_code/providers/qwen_provider.py | 15 ++-- .../providers/qwen_tool_call_parser.py | 9 ++- src/iac_code/providers/streaming.py | 15 +++- src/iac_code/services/providers/aliyun.py | 4 +- src/iac_code/services/session_backup.py | 8 ++- .../services/session_backup_staging.py | 7 +- .../ros_validation/rules/eip_association.py | 8 ++- src/iac_code/ui/components/select.py | 6 +- src/iac_code/web/events.py | 4 +- src/iac_code/web/pipeline_transcript.py | 3 +- tests/a2a/test_executor.py | 23 +++++-- tests/a2a/test_pipeline_identity.py | 18 ++--- tests/a2a/test_pipeline_snapshot.py | 4 +- tests/a2a/test_pipeline_stream.py | 6 +- .../test_start_chat_permission_wait_runner.py | 36 +++++----- tests/agui/test_app.py | 16 +++-- tests/agui/test_http_sse_integration.py | 4 +- tests/agui/test_persistence.py | 51 ++++++-------- .../engine/test_complete_step_tool.py | 4 +- .../pipeline/engine/test_hard_constraints.py | 2 - tests/pipeline/engine/test_step_executor.py | 28 +++----- .../selling/test_terminal_ui_contract.py | 4 +- .../test_completion_projection.py | 39 +++++++---- .../test_materialize_step.py | 14 ++-- .../test_pipeline_definition.py | 15 ++-- .../test_solution_planning_step.py | 26 +++---- ...st_selling_solution_first_run_scenarios.py | 16 +++-- tests/providers/test_dashscope_provider.py | 8 ++- .../test_provider_model_research_updates.py | 4 +- tests/providers/test_qwen_provider.py | 69 ++++++++++++++----- tests/providers/test_qwen_tool_call_parser.py | 8 ++- .../test_alicloud_ros_agent_bridge.py | 2 +- tests/skill_bridge/test_runtime_release.py | 4 +- .../cloud/aliyun/test_ros_validate_hook.py | 4 +- tests/web/test_diagrams.py | 12 +++- tests/web/test_frontend_static.py | 2 +- tests/web/test_pipeline_transcript.py | 3 +- tests/web/test_session_manager.py | 4 +- 60 files changed, 450 insertions(+), 247 deletions(-) diff --git a/src/iac_code/a2a/events.py b/src/iac_code/a2a/events.py index 08330a14..a5e61404 100644 --- a/src/iac_code/a2a/events.py +++ b/src/iac_code/a2a/events.py @@ -516,7 +516,11 @@ async def publish_stream_event( task_id=task_id, context_id=context_id, state=TaskState.TASK_STATE_WORKING, - metadata={"iac_code": {"usage": usage}}, + metadata={ + "iac_code": { + "usage": usage + } + }, iac_code_session_id=iac_code_session_id, ) return None diff --git a/src/iac_code/a2a/executor.py b/src/iac_code/a2a/executor.py index bfb0d893..6abcba1f 100644 --- a/src/iac_code/a2a/executor.py +++ b/src/iac_code/a2a/executor.py @@ -2923,7 +2923,9 @@ def _read(name: str) -> str | None: return None if re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", region_id) is None: language = self._resolve_preferred_language(metadata) or "en" - raise InvalidParamsError(translate_message("Unsupported Alibaba Cloud region ID.", language=language)) + raise InvalidParamsError( + translate_message("Unsupported Alibaba Cloud region ID.", language=language) + ) configured = AliyunCredentials.load() if configured is None: return None diff --git a/src/iac_code/a2a/pipeline_events.py b/src/iac_code/a2a/pipeline_events.py index 0b7d003d..42218023 100644 --- a/src/iac_code/a2a/pipeline_events.py +++ b/src/iac_code/a2a/pipeline_events.py @@ -561,7 +561,8 @@ def _completion_artifact_events(self, completed: dict[str, Any]) -> list[dict[st if conditions and ( not isinstance(conclusion, dict) or not all( - _resolve_artifact_expression(conclusion, field) == value for field, value in conditions.items() + _resolve_artifact_expression(conclusion, field) == value + for field, value in conditions.items() ) ): continue diff --git a/src/iac_code/agent/agent_loop.py b/src/iac_code/agent/agent_loop.py index 77250c66..419d4b76 100644 --- a/src/iac_code/agent/agent_loop.py +++ b/src/iac_code/agent/agent_loop.py @@ -1787,7 +1787,9 @@ def _prepare_request_lease( tools = list(self.tool_registry.list_tools()) tool_definitions = self._get_tool_definitions(tools) begin_request = ( - getattr(request_manager, "begin_request", None) if hasattr(type(request_manager), "begin_request") else None + getattr(request_manager, "begin_request", None) + if hasattr(type(request_manager), "begin_request") + else None ) lease = begin_request(base_system_prompt, tool_definitions or None) if callable(begin_request) else None try: @@ -2818,7 +2820,9 @@ async def _complete_provider_with_lease( cache_policy: str | None = None, ) -> Any: begin_request = ( - getattr(request_manager, "begin_request", None) if hasattr(type(request_manager), "begin_request") else None + getattr(request_manager, "begin_request", None) + if hasattr(type(request_manager), "begin_request") + else None ) lease = begin_request(system, tools) if callable(begin_request) else None effective_system = getattr(lease, "system_prompt", system) @@ -3057,7 +3061,11 @@ def _record_session_usage( # Compatibility for lightweight third-party/test managers that do # not implement ProviderManager's internal attribution contract. provider = self._get_runtime_provider_key() - model = self._provider_manager.get_model_name() if hasattr(self._provider_manager, "get_model_name") else "" + model = ( + self._provider_manager.get_model_name() + if hasattr(self._provider_manager, "get_model_name") + else "" + ) try: self._session_usage_store.append( self._cwd, diff --git a/src/iac_code/agui/adapter.py b/src/iac_code/agui/adapter.py index 035bfd35..eb0684d1 100644 --- a/src/iac_code/agui/adapter.py +++ b/src/iac_code/agui/adapter.py @@ -384,7 +384,8 @@ async def stream(self, ticket: RunTicket) -> AsyncIterator[Any]: pending_values = [ value for value in a2a_inputs(event) - if (binding.execution_id, str(value.get("inputId") or "")) not in binding.applied_resume_digests + if (binding.execution_id, str(value.get("inputId") or "")) + not in binding.applied_resume_digests ] if pending_values: if sideband_recovery_after is not None: @@ -910,7 +911,8 @@ async def _prepare_resume( # it accepts or rejects the response. Terminal task states always win. task_is_terminal = a2a_state(task) in _FAILED_STATES | {"canceled", "completed"} replace_pending = task_is_terminal or ( - not resumed_pending_ids and ("input" in task_metadata or "pendingPermissions" in task_metadata) + not resumed_pending_ids + and ("input" in task_metadata or "pendingPermissions" in task_metadata) ) self._merge_pending( binding, diff --git a/src/iac_code/agui/state.py b/src/iac_code/agui/state.py index 4fde0fe5..c8665dfd 100644 --- a/src/iac_code/agui/state.py +++ b/src/iac_code/agui/state.py @@ -73,7 +73,10 @@ def load_thread(self, thread_id: str) -> dict[str, Any] | None: def save_thread(self, thread_id: str, state: Mapping[str, Any]) -> None: document = dict(state) - if document.get("schemaVersion") != AGUI_STATE_SCHEMA_VERSION or document.get("threadId") != thread_id: + if ( + document.get("schemaVersion") != AGUI_STATE_SCHEMA_VERSION + or document.get("threadId") != thread_id + ): raise AguiStateStoreError("Refusing to save invalid AG-UI thread state.") try: ensure_private_dir(self.state_dir) diff --git a/src/iac_code/pipeline/constants.py b/src/iac_code/pipeline/constants.py index 635a2b63..c6dcf34a 100644 --- a/src/iac_code/pipeline/constants.py +++ b/src/iac_code/pipeline/constants.py @@ -4,7 +4,9 @@ SELLING_SOLUTION_FIRST_PIPELINE_NAME = "selling_solution_first" #: Pipelines a remote caller (POP → ros-ai-agent → A2A) may select per request. -SELECTABLE_PIPELINE_NAMES: frozenset[str] = frozenset({SELLING_PIPELINE_NAME, SELLING_SOLUTION_FIRST_PIPELINE_NAME}) +SELECTABLE_PIPELINE_NAMES: frozenset[str] = frozenset( + {SELLING_PIPELINE_NAME, SELLING_SOLUTION_FIRST_PIPELINE_NAME} +) CLEANUP_PROMPT_METADATA_TYPE = "pipeline_cleanup_prompt" diff --git a/src/iac_code/pipeline/engine/complete_step_tool.py b/src/iac_code/pipeline/engine/complete_step_tool.py index 2825ace0..ddf69637 100644 --- a/src/iac_code/pipeline/engine/complete_step_tool.py +++ b/src/iac_code/pipeline/engine/complete_step_tool.py @@ -418,7 +418,7 @@ def _raw_completion_input_schema(self) -> dict[str, Any]: schema = copy.deepcopy(self.input_schema) schema["description"] = _( 'complete_step arguments must be {"conclusion": {...}}; keep all conclusion fields, ' - "including candidates, inside conclusion and do not submit them at the tool input top level." + 'including candidates, inside conclusion and do not submit them at the tool input top level.' ) properties = schema.get("properties") if isinstance(properties, dict) and self._step_config.completion_input_schema: @@ -454,7 +454,9 @@ def _completion_input_error_detail(self, error: jsonschema.ValidationError) -> d "validator": str(error.validator or ""), "message": self._bounded_completion_validation_message(error), "expected": self._bounded_completion_expected(error.validator, error.validator_value), - "description": self._bounded_description(self._nearest_completion_input_description(absolute_path, error)), + "description": self._bounded_description( + self._nearest_completion_input_description(absolute_path, error) + ), "received": self._bounded_received(error.instance), } @@ -756,7 +758,9 @@ def _conclusion_schema_error_detail( "validator": str(error.validator or ""), "message": self._bounded_completion_validation_message(error), "expected": self._bounded_completion_expected(error.validator, error.validator_value), - "description": self._bounded_description(self._nearest_schema_description(schema, path_parts, error)), + "description": self._bounded_description( + self._nearest_schema_description(schema, path_parts, error) + ), "received": self._bounded_received(error.instance), } @@ -910,19 +914,22 @@ def _validate_structured_user_input_action( "input_type": "structured", "user_input": self._user_message, "parameter_overrides": ( - structured.parameter_overrides if structured.parameter_overrides_provided else current_overrides + structured.parameter_overrides + if structured.parameter_overrides_provided + else current_overrides ), } if confirmation != expected_confirmation: - return _("{message} complete_step.conclusion.{field} must record the exact structured input.").format( - message=base_message, field=confirmation_field - ) + return _( + "{message} complete_step.conclusion.{field} must record the exact structured input." + ).format(message=base_message, field=confirmation_field) change_required_actions = self._expected_actions( {"actions": requirement.get("require_parameter_changes_for_actions")} ) if structured.action in change_required_actions and ( - not structured.parameter_overrides_provided or structured.parameter_overrides == current_overrides + not structured.parameter_overrides_provided + or structured.parameter_overrides == current_overrides ): return base_message @@ -1542,7 +1549,9 @@ def finalize_completion_input( enriched = enricher( tool_input=copy.deepcopy(normalized_input), context_snapshot=copy.deepcopy(self._completion_guard_state.get("context_snapshot") or {}), - tool_result_records=copy.deepcopy(self._completion_guard_state.get("tool_result_records") or []), + tool_result_records=copy.deepcopy( + self._completion_guard_state.get("tool_result_records") or [] + ), user_message=self._user_message, completion_guard_state=self._completion_guard_state, config=self._step_config, @@ -1760,7 +1769,9 @@ def _string_set(value: Any) -> set[str]: async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult: del context submitted_delta = copy.deepcopy(tool_input) - projection_metadata = {"submitted_delta": submitted_delta} if self._step_config.completion_input_schema else {} + projection_metadata = ( + {"submitted_delta": submitted_delta} if self._step_config.completion_input_schema else {} + ) logger.debug( "[complete_step] step=%s input=%s", diff --git a/src/iac_code/pipeline/engine/loader.py b/src/iac_code/pipeline/engine/loader.py index 6ed537c8..964b58b0 100644 --- a/src/iac_code/pipeline/engine/loader.py +++ b/src/iac_code/pipeline/engine/loader.py @@ -347,7 +347,9 @@ def _parse_surface_overrides(raw: object, step_id: str) -> dict[str, StepSurface conclusion_schema = override.get("conclusion_schema") if conclusion_schema is not None and not isinstance(conclusion_schema, dict): - raise ValueError(f"Step '{step_id}': surface_overrides.{surface}.conclusion_schema must be a mapping") + raise ValueError( + f"Step '{step_id}': surface_overrides.{surface}.conclusion_schema must be a mapping" + ) overrides[surface] = StepSurfaceOverride( prompt_file=prompt, @@ -382,7 +384,8 @@ def _parse_a2a_artifacts(raw: object, step_id: str) -> list[A2AArtifactSpec]: has_content_from_file = isinstance(content_from_file, str) and bool(content_from_file) if has_content == has_content_from_file: raise ValueError( - f"Step '{step_id}': a2a_artifacts[{index}] must define exactly one of content or content_from_file" + f"Step '{step_id}': a2a_artifacts[{index}] must define exactly one of " + "content or content_from_file" ) if not isinstance(media_type, str) or not media_type: raise ValueError(f"Step '{step_id}': a2a_artifacts[{index}].media_type must be a non-empty string") @@ -393,7 +396,9 @@ def _parse_a2a_artifacts(raw: object, step_id: str) -> list[A2AArtifactSpec]: raw_when_equals = item.get("when_conclusion_field_equals") when_equals = {} if raw_when_equals is None else raw_when_equals if not isinstance(when_equals, dict) or not all(isinstance(key, str) and key for key in when_equals): - raise ValueError(f"Step '{step_id}': a2a_artifacts[{index}].when_conclusion_field_equals must be a mapping") + raise ValueError( + f"Step '{step_id}': a2a_artifacts[{index}].when_conclusion_field_equals must be a mapping" + ) specs.append( A2AArtifactSpec( path=path, diff --git a/src/iac_code/pipeline/engine/pipeline_runner.py b/src/iac_code/pipeline/engine/pipeline_runner.py index 1f74262d..b8caf669 100644 --- a/src/iac_code/pipeline/engine/pipeline_runner.py +++ b/src/iac_code/pipeline/engine/pipeline_runner.py @@ -2953,7 +2953,9 @@ async def resume( and step.config.get("confirmation_accepts_parameter_overrides") is True and step.validate_structured_confirmation is not None ): - validation_message = self._structured_confirmation_validation_message(step, current_conclusion, user_text) + validation_message = self._structured_confirmation_validation_message( + step, current_conclusion, user_text + ) if validation_message: # The submitted parameters are illegal, so the step keeps its waiting input untouched: no # bookkeeping is popped, no state is saved and no model turn is spent. diff --git a/src/iac_code/pipeline/engine/resume_recovery.py b/src/iac_code/pipeline/engine/resume_recovery.py index e271936f..3379b2a3 100644 --- a/src/iac_code/pipeline/engine/resume_recovery.py +++ b/src/iac_code/pipeline/engine/resume_recovery.py @@ -51,7 +51,11 @@ def _without_overridden_error_results(message: Message, successful_ids: set[str] content = [ block for block in message.content - if not (isinstance(block, ToolResultBlock) and block.is_error and block.tool_use_id in successful_ids) + if not ( + isinstance(block, ToolResultBlock) + and block.is_error + and block.tool_use_id in successful_ids + ) ] if not content: return None diff --git a/src/iac_code/pipeline/engine/step_executor.py b/src/iac_code/pipeline/engine/step_executor.py index 02485392..d61bdab3 100644 --- a/src/iac_code/pipeline/engine/step_executor.py +++ b/src/iac_code/pipeline/engine/step_executor.py @@ -526,7 +526,9 @@ def build_agent_loop_context( completion_guard_state: dict[str, Any] = ensure_completion_guard_state( reconstruct_completion_guard_state( repaired_messages, - completion_record_contract=self._optional_config_string(step.config.get("completion_record_contract")), + completion_record_contract=self._optional_config_string( + step.config.get("completion_record_contract") + ), ) ) saved_step_conclusion = context.snapshot().get(step.conclusion_field) @@ -698,7 +700,9 @@ def finalize_completion_input_from_transcript( completion_guard_state = ensure_completion_guard_state( reconstruct_completion_guard_state( list(resume_messages), - completion_record_contract=self._optional_config_string(step.config.get("completion_record_contract")), + completion_record_contract=self._optional_config_string( + step.config.get("completion_record_contract") + ), ) ) if self._cwd: @@ -1192,7 +1196,9 @@ def _build_step_tools( authoritative_candidate_targets=self._config_string_tuple( step.config.get("authoritative_candidate_targets") ), - completion_record_contract=self._optional_config_string(step.config.get("completion_record_contract")), + completion_record_contract=self._optional_config_string( + step.config.get("completion_record_contract") + ), hard_constraint_evidence_contract=self._optional_config_string( step.config.get("hard_constraint_evidence_contract") ), diff --git a/src/iac_code/pipeline/selling_solution_first/hooks/deploying.py b/src/iac_code/pipeline/selling_solution_first/hooks/deploying.py index 66fa5bc0..b794a578 100644 --- a/src/iac_code/pipeline/selling_solution_first/hooks/deploying.py +++ b/src/iac_code/pipeline/selling_solution_first/hooks/deploying.py @@ -52,7 +52,9 @@ def enrich_completion_input( tool_input["conclusion"] = {"status": "cancelled"} return tool_input records = [ - record for record in tool_result_records if isinstance(record, dict) and record.get("tool_name") == "ros_deploy" + record + for record in tool_result_records + if isinstance(record, dict) and record.get("tool_name") == "ros_deploy" ] if status == "success": record = next( diff --git a/src/iac_code/pipeline/selling_solution_first/hooks/materialize_selected_candidate.py b/src/iac_code/pipeline/selling_solution_first/hooks/materialize_selected_candidate.py index 4a64e3d8..2e828206 100644 --- a/src/iac_code/pipeline/selling_solution_first/hooks/materialize_selected_candidate.py +++ b/src/iac_code/pipeline/selling_solution_first/hooks/materialize_selected_candidate.py @@ -219,7 +219,9 @@ def enrich_completion_input( records, tool_name="ros_preview_template", after=last_mutation, - predicate=lambda record: _record_matches_anchor(record, canonical_output, effective_parameters, region, cwd), + predicate=lambda record: _record_matches_anchor( + record, canonical_output, effective_parameters, region, cwd + ), ) preview_validation = _preview_projection(preview, output_path, effective_parameters, region) quote = _quote_projection(anchor) @@ -613,7 +615,9 @@ def _quote_projection(anchor: dict[str, Any]) -> dict[str, Any]: } currency = result.get("Currency") or (next(iter(resource_currencies)) if len(resource_currencies) == 1 else "CNY") if str(currency).upper() != "CNY": - return _unavailable_quote(_("Unsupported ROS estimate currency: {currency}").format(currency=currency)) + return _unavailable_quote( + _("Unsupported ROS estimate currency: {currency}").format(currency=currency) + ) if any(item != "CNY" for item in resource_currencies): return _unavailable_quote( _("Unsupported ROS estimate resource currencies: {currencies}").format( @@ -807,15 +811,11 @@ def _confirmation_options(selection: Any) -> list[dict[str, str]]: }, } raw_candidates = selection.get("candidates") if isinstance(selection, dict) else None - actions = ( - ["confirm", "cancel"] - if not isinstance(raw_candidates, list) or len(raw_candidates) <= 1 - else [ - "confirm", - "reselect", - "cancel", - ] - ) + actions = ["confirm", "cancel"] if not isinstance(raw_candidates, list) or len(raw_candidates) <= 1 else [ + "confirm", + "reselect", + "cancel", + ] return [copy.deepcopy(defaults[action]) for action in actions] @@ -1028,11 +1028,8 @@ def _project_evidence( def _allowed_context_path(path: str, allowed: tuple[str, ...]) -> bool: - return ( - bool(path) - and ".." not in path - and "*" not in path - and any(path == prefix or path.startswith(prefix + ".") for prefix in allowed) + return bool(path) and ".." not in path and "*" not in path and any( + path == prefix or path.startswith(prefix + ".") for prefix in allowed ) diff --git a/src/iac_code/pipeline/selling_solution_first/hooks/solution_planning_and_selection.py b/src/iac_code/pipeline/selling_solution_first/hooks/solution_planning_and_selection.py index cff6337c..699137a5 100644 --- a/src/iac_code/pipeline/selling_solution_first/hooks/solution_planning_and_selection.py +++ b/src/iac_code/pipeline/selling_solution_first/hooks/solution_planning_and_selection.py @@ -347,7 +347,9 @@ def _validate_candidate_resource_intents( if missing: shown = missing[:5] suffix = ( - _("; {count} more omitted").format(count=len(missing) - len(shown)) if len(missing) > len(shown) else "" + _("; {count} more omitted").format(count=len(missing) - len(shown)) + if len(missing) > len(shown) + else "" ) raise CompletionEnrichmentError( _( @@ -359,7 +361,7 @@ def _validate_candidate_resource_intents( def _candidate_slug(name: str) -> str: slug = re.sub(r"[^a-z0-9]+", "-", name.casefold()).strip("-") - return slug[:48].rstrip("-") or "solution" + return (slug[:48].rstrip("-") or "solution") def _candidate_products(candidate: dict[str, Any]) -> list[str]: diff --git a/src/iac_code/pipeline/selling_solution_first/tools/candidate_planning_records.py b/src/iac_code/pipeline/selling_solution_first/tools/candidate_planning_records.py index 8058e0ee..56882c83 100644 --- a/src/iac_code/pipeline/selling_solution_first/tools/candidate_planning_records.py +++ b/src/iac_code/pipeline/selling_solution_first/tools/candidate_planning_records.py @@ -43,7 +43,9 @@ def latest_candidate_outline_batch(records: list[dict[str, Any]]) -> CandidateOu continue sequence = _record_sequence(record, position) recorded_candidate_set_id = record.get("candidate_set_id") - candidate_set_id = str(recorded_candidate_set_id).strip() if isinstance(recorded_candidate_set_id, str) else "" + candidate_set_id = ( + str(recorded_candidate_set_id).strip() if isinstance(recorded_candidate_set_id, str) else "" + ) record_id = record.get("record_id") if not candidate_set_id: candidate_set_id = str(record_id).strip() if isinstance(record_id, str) else "" @@ -52,7 +54,11 @@ def latest_candidate_outline_batch(records: list[dict[str, Any]]) -> CandidateOu # An identical repeated call is recorded as a successful idempotent observation with the # original candidateSetId. It must not move the active batch boundary forward, otherwise # details already produced for that batch would be incorrectly invalidated. - if latest is not None and latest.candidate_set_id == candidate_set_id and latest.candidates == candidates: + if ( + latest is not None + and latest.candidate_set_id == candidate_set_id + and latest.candidates == candidates + ): continue latest = CandidateOutlineBatch( candidate_set_id=candidate_set_id, diff --git a/src/iac_code/pipeline/selling_solution_first/tools/show_architecture_plan_tool.py b/src/iac_code/pipeline/selling_solution_first/tools/show_architecture_plan_tool.py index d74d8e7a..2325ac6c 100644 --- a/src/iac_code/pipeline/selling_solution_first/tools/show_architecture_plan_tool.py +++ b/src/iac_code/pipeline/selling_solution_first/tools/show_architecture_plan_tool.py @@ -152,7 +152,9 @@ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> "Displayed {count} candidate outlines; candidateSetId={candidate_set_id}. " "Do not repeat show_architecture_plan unless the user changes the candidate set; " "continue with show_candidate_detail." - ).format(count=len(candidates), candidate_set_id=candidate_set_id), + ).format( + count=len(candidates), candidate_set_id=candidate_set_id + ), metadata={"candidate_set_id": candidate_set_id}, ) diff --git a/src/iac_code/pipeline/selling_solution_first/tools/show_candidate_detail_tool.py b/src/iac_code/pipeline/selling_solution_first/tools/show_candidate_detail_tool.py index bcc4bc77..469f436f 100644 --- a/src/iac_code/pipeline/selling_solution_first/tools/show_candidate_detail_tool.py +++ b/src/iac_code/pipeline/selling_solution_first/tools/show_candidate_detail_tool.py @@ -177,7 +177,9 @@ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> batch = latest_candidate_outline_batch(records) if batch is None: return ToolResult.error( - _("show_candidate_detail is not allowed before a successful show_architecture_plan outline batch.") + _( + "show_candidate_detail is not allowed before a successful show_architecture_plan outline batch." + ) ) expected_index = first_missing_candidate_detail_index(records, batch) @@ -209,7 +211,9 @@ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ) try: - mermaid_source, architecture_context, warnings = render_architecture_graph(tool_input.get("topology_graph")) + mermaid_source, architecture_context, warnings = render_architecture_graph( + tool_input.get("topology_graph") + ) except ValueError as exc: return ToolResult( content=_("Failed to render the candidate topology: {reason}").format(reason=str(exc)), diff --git a/src/iac_code/providers/dashscope_endpoints.py b/src/iac_code/providers/dashscope_endpoints.py index 85f54cad..c7371ff6 100644 --- a/src/iac_code/providers/dashscope_endpoints.py +++ b/src/iac_code/providers/dashscope_endpoints.py @@ -20,7 +20,9 @@ "coding.dashscope.aliyuncs.com": "aliyun_codingplan", "coding-intl.dashscope.aliyuncs.com": "aliyun_codingplan_intl", } -_TOKEN_PLAN_REGIONS = frozenset({"cn-beijing", "ap-southeast-1", "ap-northeast-1", "eu-central-1", "us-east-1"}) +_TOKEN_PLAN_REGIONS = frozenset( + {"cn-beijing", "ap-southeast-1", "ap-northeast-1", "eu-central-1", "us-east-1"} +) def _parsed_https_endpoint(base_url: str | None) -> tuple[str, str] | None: @@ -81,7 +83,9 @@ def is_bailian_compatible_endpoint(base_url: str | None) -> bool: and _is_dns_label(labels[0]) and labels[1] in _TOKEN_PLAN_REGIONS ) - return is_maas and (_path_matches(path, "/compatible-mode") or _path_matches(path, "/apps/anthropic")) + return is_maas and ( + _path_matches(path, "/compatible-mode") or _path_matches(path, "/apps/anthropic") + ) def _is_dns_label(value: str) -> bool: diff --git a/src/iac_code/providers/manager.py b/src/iac_code/providers/manager.py index 547d3a1f..8cc59205 100644 --- a/src/iac_code/providers/manager.py +++ b/src/iac_code/providers/manager.py @@ -135,7 +135,6 @@ def _is_retryable_provider_error(exc: BaseException) -> bool: } ) - class _BestEffortSpan: def __init__(self, span: Any | None = None) -> None: self._span = span @@ -815,7 +814,9 @@ def _telemetry_provider_name(provider: Any) -> str: def _provider_telemetry_attrs(provider: Any) -> dict[str, str | bool]: attrs: dict[str, str | bool] = { - IacCodeAttr.OFFICIAL_ENDPOINT: official_dashscope_wire_provider_key(_provider_endpoint_url(provider)) + IacCodeAttr.OFFICIAL_ENDPOINT: official_dashscope_wire_provider_key( + _provider_endpoint_url(provider) + ) is not None, } adapter_name = _string_provider_attr(provider, "_ADAPTER_NAME") @@ -1789,7 +1790,9 @@ def commit_replay_failure( yield terminal_event return yield replay_event - raise UnsafeStreamProtocolError("Qwen replay ended before message completion.") + raise UnsafeStreamProtocolError( + "Qwen replay ended before message completion." + ) except UnsafeStreamProtocolError as exc: last_unsafe_error = exc commit_replay_failure(exc) diff --git a/src/iac_code/providers/openai_provider.py b/src/iac_code/providers/openai_provider.py index 4c33d8c7..36c0cd02 100644 --- a/src/iac_code/providers/openai_provider.py +++ b/src/iac_code/providers/openai_provider.py @@ -197,7 +197,9 @@ def _extract_reasoning_text(self, message_or_delta: Any) -> str: reasoning = getattr(message_or_delta, "reasoning_content", None) return reasoning if isinstance(reasoning, str) else "" - def _create_stream_response_adapter(self, tools: list[ToolDefinition] | None) -> OpenAIStreamResponseAdapter: + def _create_stream_response_adapter( + self, tools: list[ToolDefinition] | None + ) -> OpenAIStreamResponseAdapter: return OpenAIStreamResponseAdapter(self, tools) # -- Message conversion ---------------------------------------------------- diff --git a/src/iac_code/providers/qwen_prompts.py b/src/iac_code/providers/qwen_prompts.py index 220185f5..387231ec 100644 --- a/src/iac_code/providers/qwen_prompts.py +++ b/src/iac_code/providers/qwen_prompts.py @@ -25,7 +25,9 @@ def prepare_qwen_system_prompt(system: str, model: str, tools: list[ToolDefiniti parameter_name = next(iter(properties), None) if isinstance(properties, dict) else None normalized = normalized_model_name(model) if "coder" in normalized: - parameter = f"\nVALUE" if isinstance(parameter_name, str) else "" + parameter = ( + f"\nVALUE" if isinstance(parameter_name, str) else "" + ) example = f"{parameter}" elif "-vl" in normalized or normalized.endswith("vl") or "qwen-vl" in normalized: arguments = {parameter_name: "VALUE"} if isinstance(parameter_name, str) else {} diff --git a/src/iac_code/providers/qwen_provider.py b/src/iac_code/providers/qwen_provider.py index 176b652d..b5061abf 100644 --- a/src/iac_code/providers/qwen_provider.py +++ b/src/iac_code/providers/qwen_provider.py @@ -118,11 +118,14 @@ def _build_thinking_kwargs_with_mandatory(self, mandatory: bool) -> dict[str, An disabled = ( not resolved_enabled if resolved_enabled is not None - else self._thinking_disabled() or effort in {"none", "off", "disable", "disabled", "false", "0"} + else self._thinking_disabled() + or effort in {"none", "off", "disable", "disabled", "false", "0"} ) effort_is_disable = effort in {"none", "off", "disable", "disabled", "false", "0"} concrete_priority = ( - self._thinking_intent.effort.priority if dominant == "effort" else self._thinking_intent.budget.priority + self._thinking_intent.effort.priority + if dominant == "effort" + else self._thinking_intent.budget.priority ) if ( dominant in {"effort", "budget"} @@ -173,11 +176,15 @@ def _build_legacy_qwen_thinking_kwargs(self, spec: Any, *, mandatory: bool) -> d effort_is_disable = effort in {"none", "off", "disable", "disabled", "false", "0"} resolved_enabled = self._thinking_intent.enabled.value disabled = ( - not resolved_enabled if resolved_enabled is not None else self._thinking_disabled() or effort_is_disable + not resolved_enabled + if resolved_enabled is not None + else self._thinking_disabled() or effort_is_disable ) dominant = self._thinking_intent.dominant_concrete_field() concrete_priority = ( - self._thinking_intent.effort.priority if dominant == "effort" else self._thinking_intent.budget.priority + self._thinking_intent.effort.priority + if dominant == "effort" + else self._thinking_intent.budget.priority ) if ( dominant in {"effort", "budget"} diff --git a/src/iac_code/providers/qwen_tool_call_parser.py b/src/iac_code/providers/qwen_tool_call_parser.py index 043b6d0d..c3b713d2 100644 --- a/src/iac_code/providers/qwen_tool_call_parser.py +++ b/src/iac_code/providers/qwen_tool_call_parser.py @@ -299,7 +299,8 @@ def recover_xml_tool_calls(text: str, tools: list[ToolDefinition] | None) -> Xml contained = [ item for item in recovered_ranges - if wrapper_start + wrapper.start("body") <= item[0] and item[1] <= wrapper_start + wrapper.end("body") + if wrapper_start + wrapper.start("body") <= item[0] + and item[1] <= wrapper_start + wrapper.end("body") ] if not contained: return None @@ -384,7 +385,11 @@ def _position_inside_markdown_fence( length = len(match.group(1)) if open_fence is None: open_fence = (delimiter, length) - elif open_fence[0] == delimiter and length >= open_fence[1] and not line[match.end() :].strip(): + elif ( + open_fence[0] == delimiter + and length >= open_fence[1] + and not line[match.end() :].strip() + ): open_fence = None return open_fence is not None diff --git a/src/iac_code/providers/streaming.py b/src/iac_code/providers/streaming.py index e87b720f..2c842c72 100644 --- a/src/iac_code/providers/streaming.py +++ b/src/iac_code/providers/streaming.py @@ -332,7 +332,11 @@ def _process_fence_line(self, line: str) -> None: if self._fence_delimiter is None: self._fence_delimiter = delimiter self._fence_length = length - elif self._fence_delimiter == delimiter and length >= self._fence_length and not line[match.end() :].strip(): + elif ( + self._fence_delimiter == delimiter + and length >= self._fence_length + and not line[match.end() :].strip() + ): self._fence_delimiter = None self._fence_length = 0 @@ -389,7 +393,10 @@ def finalize_tags( raise UnsafeStreamProtocolError("Qwen emitted an unsafe or conflicting thinking-tag block.") closing = self._STANDALONE_CLOSING.fullmatch(literal) safe_closing = ( - finish_reason == "tool_calls" and bool(native_calls) and not reasoning_has_tag and closing is not None + finish_reason == "tool_calls" + and bool(native_calls) + and not reasoning_has_tag + and closing is not None ) if safe_closing: return [], True @@ -442,7 +449,9 @@ def feed(self, delta: Any, finish_reason: str | None) -> list[StreamEvent]: else: last_open = tag_probe.rfind("<") possible = tag_probe[last_open:] if last_open >= 0 else "" - self._reasoning_tag_probe = possible if self._guard._is_possible_tag_prefix(possible) else "" + self._reasoning_tag_probe = ( + possible if self._guard._is_possible_tag_prefix(possible) else "" + ) events.append(ThinkingDeltaEvent(text=normalized_reasoning)) content = getattr(delta, "content", None) normalized_content = self._content_normalizer.feed(content) if isinstance(content, str) and content else "" diff --git a/src/iac_code/services/providers/aliyun.py b/src/iac_code/services/providers/aliyun.py index e0119007..a66f6d59 100644 --- a/src/iac_code/services/providers/aliyun.py +++ b/src/iac_code/services/providers/aliyun.py @@ -298,7 +298,9 @@ def refresh_oauth_if_needed( owns_client = oauth_client is None client = ( - AliyunOAuthClient(get_oauth_site(credential.oauth_site_type)) if oauth_client is None else oauth_client + AliyunOAuthClient(get_oauth_site(credential.oauth_site_type)) + if oauth_client is None + else oauth_client ) try: diff --git a/src/iac_code/services/session_backup.py b/src/iac_code/services/session_backup.py index 5fea3052..5d7e0c9f 100644 --- a/src/iac_code/services/session_backup.py +++ b/src/iac_code/services/session_backup.py @@ -423,7 +423,9 @@ def reconcile_session( minimum_generation: int | None = None, ) -> SessionReconcileResult: if minimum_generation is not None and ( - isinstance(minimum_generation, bool) or not isinstance(minimum_generation, int) or minimum_generation <= 0 + isinstance(minimum_generation, bool) + or not isinstance(minimum_generation, int) + or minimum_generation <= 0 ): raise ValueError("minimum_generation must be a positive integer") if not self._backup_enabled(): @@ -448,7 +450,9 @@ def reconcile_session( if shared is not None else None ) - if minimum_generation is not None and (local_state is None or local_state.generation < minimum_generation): + if minimum_generation is not None and ( + local_state is None or local_state.generation < minimum_generation + ): if shared_state is None or shared_state.generation < minimum_generation: raise SessionBackupNotReadyError( minimum_generation=minimum_generation, diff --git a/src/iac_code/services/session_backup_staging.py b/src/iac_code/services/session_backup_staging.py index fd2bdc3e..fc0f4cff 100644 --- a/src/iac_code/services/session_backup_staging.py +++ b/src/iac_code/services/session_backup_staging.py @@ -149,7 +149,8 @@ def backup_session( existing = self._read_existing_snapshot_state(destination, session_id) if existing is not None: completed_next = ( - base_state.status == "succeeded" and existing.parent_generation == base_state.generation + base_state.status == "succeeded" + and existing.parent_generation == base_state.generation ) if not completed_next and not existing.same_lineage(committed_state): raise SessionBackupConflict( @@ -248,7 +249,9 @@ def reconcile_session( minimum_generation: int | None = None, ) -> SessionReconcileResult: if minimum_generation is not None and ( - isinstance(minimum_generation, bool) or not isinstance(minimum_generation, int) or minimum_generation <= 0 + isinstance(minimum_generation, bool) + or not isinstance(minimum_generation, int) + or minimum_generation <= 0 ): raise ValueError("minimum_generation must be a positive integer") if not self._backup_enabled(): diff --git a/src/iac_code/tools/cloud/aliyun/ros_validation/rules/eip_association.py b/src/iac_code/tools/cloud/aliyun/ros_validation/rules/eip_association.py index 3711e05f..567f166b 100644 --- a/src/iac_code/tools/cloud/aliyun/ros_validation/rules/eip_association.py +++ b/src/iac_code/tools/cloud/aliyun/ros_validation/rules/eip_association.py @@ -65,7 +65,9 @@ def check(self, context: Any) -> tuple[Diagnostic, ...]: ecs_resources = { name: definition for name, definition in resources.items() - if isinstance(name, str) and isinstance(definition, Mapping) and definition.get("Type") in _ECS_TYPES + if isinstance(name, str) + and isinstance(definition, Mapping) + and definition.get("Type") in _ECS_TYPES } diagnostics: list[Diagnostic] = [] for association_name, association in resources.items(): @@ -81,7 +83,9 @@ def check(self, context: Any) -> tuple[Diagnostic, ...]: target = ecs_resources[target_name] target_properties = target.get("Properties") allocate_public_ip = ( - target_properties.get("AllocatePublicIP") if isinstance(target_properties, Mapping) else None + target_properties.get("AllocatePublicIP") + if isinstance(target_properties, Mapping) + else None ) if _is_explicit_false(allocate_public_ip): continue diff --git a/src/iac_code/ui/components/select.py b/src/iac_code/ui/components/select.py index cdf582ad..7733415b 100644 --- a/src/iac_code/ui/components/select.py +++ b/src/iac_code/ui/components/select.py @@ -184,9 +184,9 @@ def handle_key(self, key_event: KeyEvent) -> bool: # Optional type-to-edit behavior for selectors whose final row is an inline input. focused_option = self._options[self.state.focused_index] if self._options else None - direct_text = (len(key_event.char) == 1 and key_event.char.isprintable()) or ( - key_event.key == "paste" and bool(key_event.char) - ) + direct_text = ( + len(key_event.char) == 1 and key_event.char.isprintable() + ) or (key_event.key == "paste" and bool(key_event.char)) if ( self._type_to_edit_input and isinstance(focused_option, InputOption) diff --git a/src/iac_code/web/events.py b/src/iac_code/web/events.py index 368c9953..a51f4e13 100644 --- a/src/iac_code/web/events.py +++ b/src/iac_code/web/events.py @@ -413,7 +413,9 @@ def translate_stream_event(self, event: StreamEvent, *, turn_id: str) -> dict[st summary=event.result, artifacts=[public_metadata] if public_metadata else [], submitted_delta=submitted_delta if isinstance(submitted_delta, dict) else None, - normalized_conclusion=(normalized_conclusion if isinstance(normalized_conclusion, dict) else None), + normalized_conclusion=( + normalized_conclusion if isinstance(normalized_conclusion, dict) else None + ), ) if isinstance(event, MCPProgressEvent): payload = mcp_progress_metadata(event) diff --git a/src/iac_code/web/pipeline_transcript.py b/src/iac_code/web/pipeline_transcript.py index 05cc6c7c..445ed2b6 100644 --- a/src/iac_code/web/pipeline_transcript.py +++ b/src/iac_code/web/pipeline_transcript.py @@ -1522,7 +1522,8 @@ def build_pipeline_transcript_rows(envelopes: Iterable[Mapping[str, Any]]) -> li # weave persisted ``source=pipeline`` replies right after their prompt rather # than appending them after the whole replay (Issue 2 misordering). anchors = translator.input_prompt_anchors or [ - {"messageId": message_id, "kind": "", "stepId": ""} for message_id in translator.input_prompt_message_ids + {"messageId": message_id, "kind": "", "stepId": ""} + for message_id in translator.input_prompt_message_ids ] for anchor in anchors: if anchor.get("expectsVisibleAnswer") == "0": diff --git a/tests/a2a/test_executor.py b/tests/a2a/test_executor.py index 0ff2c028..feb8eb8a 100644 --- a/tests/a2a/test_executor.py +++ b/tests/a2a/test_executor.py @@ -2884,7 +2884,9 @@ async def test_executor_runs_normal_mode_when_iac_code_mode_is_normal( @pytest.mark.asyncio -async def test_normal_mode_ignores_stale_pipeline_name(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: +async def test_normal_mode_ignores_stale_pipeline_name( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: monkeypatch.setenv("IAC_CODE_MODE", "pipeline") loop = FakeAgentLoop([TextDeltaEvent(text="normal")]) runtime = FakeRuntime(agent_loop=loop, session_id="session-1") @@ -4292,7 +4294,9 @@ def test_region_only_metadata_copies_configured_credential_without_mutating_it( ) executor = self._make_executor() - result = executor._resolve_aliyun_credential({"iac_code": {"alibaba_cloud_region_id": "cn-beijing"}}) + result = executor._resolve_aliyun_credential( + {"iac_code": {"alibaba_cloud_region_id": "cn-beijing"}} + ) assert result is not None assert result is not configured @@ -4308,7 +4312,9 @@ def test_region_only_metadata_returns_none_without_configured_credential( monkeypatch.setattr("iac_code.a2a.executor.AliyunCredentials.load", lambda: None) executor = self._make_executor() - result = executor._resolve_aliyun_credential({"iac_code": {"alibaba_cloud_region_id": "cn-beijing"}}) + result = executor._resolve_aliyun_credential( + {"iac_code": {"alibaba_cloud_region_id": "cn-beijing"}} + ) assert result is None @@ -4316,7 +4322,9 @@ def test_region_only_metadata_rejects_invalid_region(self) -> None: executor = self._make_executor() with pytest.raises(InvalidParamsError, match="Unsupported Alibaba Cloud region ID"): - executor._resolve_aliyun_credential({"iac_code": {"alibaba_cloud_region_id": "https://example.com"}}) + executor._resolve_aliyun_credential( + {"iac_code": {"alibaba_cloud_region_id": "https://example.com"}} + ) @pytest.mark.asyncio @@ -4974,7 +4982,9 @@ async def test_persisted_permission_restores_backup_before_checkpoint_lookup( response=response, ) assert restored_storage.exists(cwd, session_id) - assert (restored_storage.session_dir(cwd, session_id) / "permission-waits" / f"{boundary_id}.json").is_file() + assert ( + restored_storage.session_dir(cwd, session_id) / "permission-waits" / f"{boundary_id}.json" + ).is_file() @pytest.mark.asyncio @@ -5296,7 +5306,8 @@ def register_cloud_tools(_registry, credentials, _services): index for index, event in enumerate(queue.events) if isinstance(event, TaskStatusUpdateEvent) - and dump(event).get("metadata", {}).get("iac_code", {}).get("inputReceived", {}).get("decision") == "allow_once" + and dump(event).get("metadata", {}).get("iac_code", {}).get("inputReceived", {}).get("decision") + == "allow_once" ] final_indices = [ index diff --git a/tests/a2a/test_pipeline_identity.py b/tests/a2a/test_pipeline_identity.py index 04454055..57760409 100644 --- a/tests/a2a/test_pipeline_identity.py +++ b/tests/a2a/test_pipeline_identity.py @@ -436,7 +436,9 @@ def test_two_sessions_run_different_pipelines_in_one_process( solution_executor = _inner_executor(pipeline_name=SELLING_SOLUTION_FIRST_PIPELINE_NAME) assert ( - legacy_executor._resolve_request_pipeline_name(cwd=cwd, session_id=legacy_session, session_storage=storage) + legacy_executor._resolve_request_pipeline_name( + cwd=cwd, session_id=legacy_session, session_storage=storage + ) == SELLING_PIPELINE_NAME ) assert ( @@ -582,6 +584,7 @@ def boom(*_args: Any, **_kwargs: Any) -> Any: assert snapshot_path.read_bytes() == snapshot_before assert not ctx.lock.locked() + @pytest.mark.asyncio @pytest.mark.parametrize("active_followup_only", [True, False], ids=["followup-probe", "full-request"]) async def test_a_mismatch_on_an_active_task_never_reaches_the_running_pipeline( @@ -726,14 +729,11 @@ def test_sidecar_identity_written_by_the_engine_is_readable_by_the_guard( raw = yaml.safe_load(meta_path.read_text(encoding="utf-8")) assert raw["pipeline_name"] == SELLING_SOLUTION_FIRST_PIPELINE_NAME - assert ( - _inner_executor()._peek_durable_pipeline_name( - cwd=cwd, - session_id=session_id, - session_storage=storage, - ) - == SELLING_SOLUTION_FIRST_PIPELINE_NAME - ) + assert _inner_executor()._peek_durable_pipeline_name( + cwd=cwd, + session_id=session_id, + session_storage=storage, + ) == SELLING_SOLUTION_FIRST_PIPELINE_NAME def test_concurrent_executors_resolve_independently(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/a2a/test_pipeline_snapshot.py b/tests/a2a/test_pipeline_snapshot.py index da0a5781..24cbee66 100644 --- a/tests/a2a/test_pipeline_snapshot.py +++ b/tests/a2a/test_pipeline_snapshot.py @@ -939,7 +939,9 @@ def test_reduce_public_narrative_shape_survives_incremental_reduction() -> None: def test_reduce_legacy_message_keeps_marker_fallback_after_resume() -> None: existing = reduce_pipeline_events([]) - existing["display"]["messages"] = [{"scope": "step", "runId": "step-a-1", "round": 1, "text": "legacy public text"}] + existing["display"]["messages"] = [ + {"scope": "step", "runId": "step-a-1", "round": 1, "text": "legacy public text"} + ] resumed = reduce_pipeline_events( [ diff --git a/tests/a2a/test_pipeline_stream.py b/tests/a2a/test_pipeline_stream.py index 76e34b70..c2b22f60 100644 --- a/tests/a2a/test_pipeline_stream.py +++ b/tests/a2a/test_pipeline_stream.py @@ -1850,7 +1850,9 @@ async def test_publish_file_backed_completion_artifact_keeps_body_only_in_artifa if dump(event).get("metadata", {}).get("iac_code", {}).get("pipeline", {}).get("eventType") ] artifact_status = next(event for event in status_events if event["eventType"] == "artifact_created") - journal_artifact = next(event for event in publisher.journal.read_all() if event["eventType"] == "artifact_created") + journal_artifact = next( + event for event in publisher.journal.read_all() if event["eventType"] == "artifact_created" + ) snapshot = publisher.snapshot_store.load() assert snapshot is not None snapshot_artifact = snapshot["display"]["artifacts"][0] @@ -1862,7 +1864,7 @@ async def test_publish_file_backed_completion_artifact_keeps_body_only_in_artifa assert snapshot_artifact["dedupeKey"] == artifact_status["artifact"]["dedupeKey"] for persisted in (artifact_status, journal_artifact, snapshot): assert template_body not in str(persisted) - assert '"content"' not in str(persisted) + assert "\"content\"" not in str(persisted) @pytest.mark.asyncio diff --git a/tests/a2a_e2e/test_start_chat_permission_wait_runner.py b/tests/a2a_e2e/test_start_chat_permission_wait_runner.py index b2d08c2a..11dbd7fc 100644 --- a/tests/a2a_e2e/test_start_chat_permission_wait_runner.py +++ b/tests/a2a_e2e/test_start_chat_permission_wait_runner.py @@ -653,25 +653,25 @@ def test_real_runner_records_assistant_diagram_and_cloud_permission_block_order( stdout = "\n".join( json.dumps(item) for item in ( - { - "type": "assistant", - "message": { - "content": [ - {"type": "text", "text": "```mermaid\ngraph TD\n```"}, - { - "type": "tool_use", - "id": "bridge-1", - "name": "Bash", - "input": { - "command": ( - "ALICLOUD_ROS_AGENT_STATE_DIR=/tmp/state python3 " - "/repo/skills/alicloud-ros-agent/scripts/ros_agent.py " - "start --prompt-file request.txt --mode normal --follow" - ) - }, + { + "type": "assistant", + "message": { + "content": [ + {"type": "text", "text": "```mermaid\ngraph TD\n```"}, + { + "type": "tool_use", + "id": "bridge-1", + "name": "Bash", + "input": { + "command": ( + "ALICLOUD_ROS_AGENT_STATE_DIR=/tmp/state python3 " + "/repo/skills/alicloud-ros-agent/scripts/ros_agent.py " + "start --prompt-file request.txt --mode normal --follow" + ) }, - ] - }, + }, + ] + }, }, { "type": "user", diff --git a/tests/agui/test_app.py b/tests/agui/test_app.py index c88f73c7..fad0388c 100644 --- a/tests/agui/test_app.py +++ b/tests/agui/test_app.py @@ -657,7 +657,9 @@ class SyncingPreflightClient(FakeA2AClient): async def ensure_session_restored(self, _url, *, cwd, session_id, task_id=None): self.restored_sessions.append((cwd, session_id, task_id)) self.resume_preflight_calls.append("ensure_session_restored") - raise A2ASessionBackupNotReadyError("Session backup is still synchronizing. Retry after 3 seconds.") + raise A2ASessionBackupNotReadyError( + "Session backup is still synchronizing. Retry after 3 seconds." + ) fake = SyncingPreflightClient(interrupt=True) adapter = AguiA2AAdapter(a2a_url="http://a2a/", client=fake) @@ -974,10 +976,10 @@ async def test_question_selection_resume_is_sent_to_same_a2a_task(tmp_path, monk assert _events(first)[-1]["outcome"]["type"] == "interrupt" assert fake.resumed_prompts == [("Plan B", "task-1")] second_events = _events(second) - assert ( - sum(event.get("type") == "TOOL_CALL_RESULT" and event.get("toolCallId") == "ask-1" for event in second_events) - == 1 - ) + assert sum( + event.get("type") == "TOOL_CALL_RESULT" and event.get("toolCallId") == "ask-1" + for event in second_events + ) == 1 assert second_events[-1]["outcome"] == {"type": "success"} @@ -1173,7 +1175,9 @@ def test_mapper_consumes_real_local_a2a_wire_contract(tmp_path, monkeypatch) -> }, ) as response: raw_events = [ - json.loads(line.removeprefix("data: ")) for line in response.iter_lines() if line.startswith("data: ") + json.loads(line.removeprefix("data: ")) + for line in response.iter_lines() + if line.startswith("data: ") ] mapper = A2AEventMapper(thread_id="thread-1", run_id="run-1") diff --git a/tests/agui/test_http_sse_integration.py b/tests/agui/test_http_sse_integration.py index dee911aa..8743b84a 100644 --- a/tests/agui/test_http_sse_integration.py +++ b/tests/agui/test_http_sse_integration.py @@ -220,7 +220,9 @@ async def record_cancel(self, context, event_queue): "TASK_STATE_CANCELED", } assert cancel_calls == [] - persisted = json.loads((state_dir / "threads" / "thread-http-sse.json").read_text(encoding="utf-8")) + persisted = json.loads( + (state_dir / "threads" / "thread-http-sse.json").read_text(encoding="utf-8") + ) assert persisted["execution"]["pending"][interrupt_id]["sideband"] is True second_agui = create_agui_app(a2a_url=a2a_url, state_dir=state_dir) diff --git a/tests/agui/test_persistence.py b/tests/agui/test_persistence.py index 97b108c0..6dd3925f 100644 --- a/tests/agui/test_persistence.py +++ b/tests/agui/test_persistence.py @@ -1154,16 +1154,13 @@ async def test_shared_state_dir_keeps_interleaved_threads_isolated(tmp_path: Pat second_payload["threadId"] = "thread-2" second_payload["forwardedProps"]["iacCode"]["rosInvocationId"] = "invocation-2" - async with ( - httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=first_adapter)), - base_url="http://test", - ) as first_client, - httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=second_adapter)), - base_url="http://test", - ) as second_client, - ): + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=first_adapter)), + base_url="http://test", + ) as first_client, httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=second_adapter)), + base_url="http://test", + ) as second_client: first_response = await first_client.post("/", json=first_payload) second_response = await second_client.post("/", json=second_payload) @@ -1181,16 +1178,13 @@ async def test_shared_state_dir_keeps_interleaved_threads_isolated(tmp_path: Pat restarted_first = AguiA2AAdapter(a2a_url="http://a2a/", client=FakeA2AClient(), state_dir=state_dir) restarted_second = AguiA2AAdapter(a2a_url="http://a2a/", client=FakeA2AClient(), state_dir=state_dir) - async with ( - httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=restarted_first)), - base_url="http://test", - ) as first_client, - httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=restarted_second)), - base_url="http://test", - ) as second_client, - ): + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=restarted_first)), + base_url="http://test", + ) as first_client, httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=restarted_second)), + base_url="http://test", + ) as second_client: first_replay = await first_client.post("/", json=first_payload) second_replay = await second_client.post("/", json=second_payload) @@ -1209,16 +1203,13 @@ async def test_corrupt_thread_state_does_not_block_another_thread(tmp_path: Path second_payload = _payload(tmp_path) second_payload["threadId"] = "thread-2" second_payload["forwardedProps"]["iacCode"]["rosInvocationId"] = "invocation-2" - async with ( - httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=first_adapter)), - base_url="http://test", - ) as first_client, - httpx.AsyncClient( - transport=httpx.ASGITransport(app=create_app(adapter=second_adapter)), - base_url="http://test", - ) as second_client, - ): + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=first_adapter)), + base_url="http://test", + ) as first_client, httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(adapter=second_adapter)), + base_url="http://test", + ) as second_client: await first_client.post("/", json=first_payload) await second_client.post("/", json=second_payload) diff --git a/tests/pipeline/engine/test_complete_step_tool.py b/tests/pipeline/engine/test_complete_step_tool.py index 629cf27e..827d4e26 100644 --- a/tests/pipeline/engine/test_complete_step_tool.py +++ b/tests/pipeline/engine/test_complete_step_tool.py @@ -335,7 +335,9 @@ async def test_authoritative_candidate_is_injected_into_both_handoff_fields(self authoritative_candidate_context_field="solution_selection.selected_candidate", authoritative_candidate_targets=("selected_candidate", "selected_candidate_result.candidate"), ), - completion_guard_state={"context_snapshot": {"solution_selection": {"selected_candidate": candidate}}}, + completion_guard_state={ + "context_snapshot": {"solution_selection": {"selected_candidate": candidate}} + }, ) result = await tool.execute( diff --git a/tests/pipeline/engine/test_hard_constraints.py b/tests/pipeline/engine/test_hard_constraints.py index 819b3ced..0ccf42c3 100644 --- a/tests/pipeline/engine/test_hard_constraints.py +++ b/tests/pipeline/engine/test_hard_constraints.py @@ -151,8 +151,6 @@ def test_validate_checks_accepts_llm_pass_when_code_verification_fails(): ) == [] ) - - def test_v2_accepts_llm_pass_when_tool_has_no_resolvable_evidence(): constraint = _constraint(verification_mode="tool") llm_passed = _check(constraint, status="satisfied", evidence=[]) diff --git a/tests/pipeline/engine/test_step_executor.py b/tests/pipeline/engine/test_step_executor.py index 84e6087d..8cd32123 100644 --- a/tests/pipeline/engine/test_step_executor.py +++ b/tests/pipeline/engine/test_step_executor.py @@ -3064,15 +3064,11 @@ def test_rich_candidate_resume_uses_compact_schema_and_preserves_first_conclusio surface="a2a_rich", ) - tool_schema = ( - executor._build_step_tools( - step, - context, - compact_candidate_selection=True, - ) - .get("complete_step") - .input_schema - ) + tool_schema = executor._build_step_tools( + step, + context, + compact_candidate_selection=True, + ).get("complete_step").input_schema conclusion_schema = tool_schema["properties"]["conclusion"] assert conclusion_schema["required"] == [ "selected_candidate_name", @@ -3155,15 +3151,11 @@ def test_stale_candidate_conclusion_cannot_enable_compact_resume_schema(self, tm context, resume_candidate_selection=True, ) - tool_schema = ( - executor._build_step_tools( - step, - context, - compact_candidate_selection=preserved is not None, - ) - .get("complete_step") - .input_schema - ) + tool_schema = executor._build_step_tools( + step, + context, + compact_candidate_selection=preserved is not None, + ).get("complete_step").input_schema assert preserved is None conclusion_schema = tool_schema["properties"]["conclusion"] diff --git a/tests/pipeline/selling/test_terminal_ui_contract.py b/tests/pipeline/selling/test_terminal_ui_contract.py index ad9382b7..c4e2cd6a 100644 --- a/tests/pipeline/selling/test_terminal_ui_contract.py +++ b/tests/pipeline/selling/test_terminal_ui_contract.py @@ -133,7 +133,9 @@ def test_confirm_prompt_tells_model_to_preserve_parameter_overrides(): def test_confirm_prompts_share_selection_contract_structure(): repl_prompt = (_selling_pipeline_dir() / "prompts" / "confirm_and_select.md").read_text(encoding="utf-8") a2a_prompt = (_selling_pipeline_dir() / "prompts" / "confirm_and_select.a2a.md").read_text(encoding="utf-8") - rich_prompt = (_selling_pipeline_dir() / "prompts" / "confirm_and_select.a2a.rich.md").read_text(encoding="utf-8") + rich_prompt = (_selling_pipeline_dir() / "prompts" / "confirm_and_select.a2a.rich.md").read_text( + encoding="utf-8" + ) shared_fragments = [ "## 首次执行", diff --git a/tests/pipeline/selling_solution_first/test_completion_projection.py b/tests/pipeline/selling_solution_first/test_completion_projection.py index a0b312ea..561f65c5 100644 --- a/tests/pipeline/selling_solution_first/test_completion_projection.py +++ b/tests/pipeline/selling_solution_first/test_completion_projection.py @@ -71,7 +71,9 @@ def _step_config(step) -> StepConfig: completion_record_contract=step.config.get("completion_record_contract"), hard_constraint_evidence_contract=step.config.get("hard_constraint_evidence_contract"), completion_context_paths=tuple(step.config.get("completion_context_paths", [])), - confirmation_accepts_parameter_overrides=(step.config.get("confirmation_accepts_parameter_overrides") is True), + confirmation_accepts_parameter_overrides=( + step.config.get("confirmation_accepts_parameter_overrides") is True + ), ) @@ -501,7 +503,10 @@ def test_model_schema_keeps_nested_structure_without_annotations_or_runtime_fiel def test_final_model_tool_schemas_stay_within_measured_token_budgets(self, loaded): counter = TokenCounter(model="deepseek-v4-flash-0731") - counts = {step.step_id: counter.count_tool_definition(_tool(step)) for step in loaded.steps} + counts = { + step.step_id: counter.count_tool_definition(_tool(step)) + for step in loaded.steps + } # Step 1 complete_step 只提交步骤语义;候选详情由展示工具记录承载。 assert counts["solution_planning_and_selection"] <= 400 @@ -567,7 +572,8 @@ def test_raw_error_returns_at_most_five_field_diagnostics(self): "type": "object", "required": field_names, "properties": { - name: {"type": "string", "description": f"Description for {name}."} for name in field_names + name: {"type": "string", "description": f"Description for {name}."} + for name in field_names }, "additionalProperties": False, }, @@ -894,10 +900,9 @@ def test_persuasion_entries_are_trimmed_and_survive_reopen_as_flat_fields(self, {"conclusion": {"status": "awaiting_selection"}}, ) assert reopened.conclusion["candidates"][0]["why_recommended"] == ["用户点名要托管数据库"] - assert ( - reopened.conclusion["candidates"][0]["problems_solved"] - == awaiting.conclusion["candidates"][0]["problems_solved"] - ) + assert reopened.conclusion["candidates"][0]["problems_solved"] == awaiting.conclusion["candidates"][0][ + "problems_solved" + ] @pytest.mark.asyncio async def test_execute_preserves_submitted_delta_and_returns_normalized_result(self, loaded): @@ -1443,7 +1448,9 @@ def test_ros_resource_mapping_is_normalized_to_monthly_total_and_details(self, l cost = result.conclusion["selected_candidate_result"]["cost"] assert cost["quote_status"] == "succeeded" - assert cost["monthly_estimate"] == ("¥360.00/month (list price; about ¥88.80/month after contract discount)") + assert cost["monthly_estimate"] == ( + "¥360.00/month (list price; about ¥88.80/month after contract discount)" + ) assert cost["resources"] == [ { "type": "Instance", @@ -1490,7 +1497,9 @@ def test_ros_subscription_period_total_is_normalized_to_monthly_price(self, load cost = result.conclusion["selected_candidate_result"]["cost"] assert cost["quote_status"] == "succeeded" - assert cost["monthly_estimate"] == ("¥630.00/month (list price; about ¥208.84/month after contract discount)") + assert cost["monthly_estimate"] == ( + "¥630.00/month (list price; about ¥208.84/month after contract discount)" + ) assert cost["resources"] == [ { "type": "DBInstance", @@ -1608,7 +1617,9 @@ def test_template_evidence_uses_ros_aware_yaml_and_internal_dotted_path(self, lo _write_template_with_intrinsic(tmp_path) step = _step(loaded, "materialize_selected_candidate") check = _check() - check["evidence"] = [{"type": "template", "template_path": "Parameters.DBInstanceStorage.Default"}] + check["evidence"] = [ + {"type": "template", "template_path": "Parameters.DBInstanceStorage.Default"} + ] result = _finalize( _tool( @@ -1619,7 +1630,9 @@ def test_template_evidence_uses_ros_aware_yaml_and_internal_dotted_path(self, lo ), _waiting_delta(check=check), ) - evidence = result.conclusion["selected_candidate_result"]["cost"]["hard_constraint_checks"][0]["evidence"][0] + evidence = result.conclusion["selected_candidate_result"]["cost"]["hard_constraint_checks"][0][ + "evidence" + ][0] assert evidence["actual_value"] == 120 assert evidence["template_path"] == "Parameters.DBInstanceStorage.Default" @@ -1638,7 +1651,9 @@ def test_context_evidence_is_allowlisted_and_resolved_by_python(self, loaded, tm ), _waiting_delta(check=_check(evidence_type="context")), ) - evidence = result.conclusion["selected_candidate_result"]["cost"]["hard_constraint_checks"][0]["evidence"][0] + evidence = result.conclusion["selected_candidate_result"]["cost"]["hard_constraint_checks"][0][ + "evidence" + ][0] assert evidence == { "type": "context", diff --git a/tests/pipeline/selling_solution_first/test_materialize_step.py b/tests/pipeline/selling_solution_first/test_materialize_step.py index 41200414..a90b35fa 100644 --- a/tests/pipeline/selling_solution_first/test_materialize_step.py +++ b/tests/pipeline/selling_solution_first/test_materialize_step.py @@ -143,9 +143,9 @@ def prompt_text() -> str: @pytest.fixture(scope="module") def skill_text() -> str: - return (_pipeline_dir() / "skills" / "iac-aliyun-materialize-selected-candidate" / "SKILL.md").read_text( - encoding="utf-8" - ) + return ( + _pipeline_dir() / "skills" / "iac-aliyun-materialize-selected-candidate" / "SKILL.md" + ).read_text(encoding="utf-8") def _conclusion(**overrides): @@ -293,7 +293,9 @@ def test_natural_language_confirmation_accepts_only_the_incremental_branch(self, assert tool.validate_completion_input(tool_input) is None assert "selected_candidate" not in tool_input["conclusion"] assert "candidate" not in tool_input["conclusion"]["selected_candidate_result"] - assert tool_input["conclusion"]["selected_candidate_result"]["cost"]["monthly_estimate"].startswith("¥1,280") + assert tool_input["conclusion"]["selected_candidate_result"]["cost"]["monthly_estimate"].startswith( + "¥1,280" + ) def test_confirmation_cannot_skip_the_dedicated_waiting_state(self, step, tmp_path): state = _happy_guard_state(str(tmp_path)) @@ -417,7 +419,7 @@ def test_structured_confirm_with_empty_overrides_preserves_current_overrides(sel "input_type": "structured", "user_input": user_message, "parameter_overrides": {"ZoneId": "cn-hangzhou-h"}, - }, + } ) tool = _tool(step, state, user_message=user_message) @@ -824,7 +826,7 @@ def test_dedicated_confirmation_supports_structured_and_natural_language_input(s def test_parameter_adjustment_reprices_and_rewrites_the_solution_summary(self, skill_text): assert "重新执行必要的参数约束查询、PreviewStack 和 ROS 精确询价" in skill_text assert "重新生成 `solution_summary`" in skill_text - assert '再次提交 `status: "awaiting_confirmation"`' in skill_text + assert "再次提交 `status: \"awaiting_confirmation\"`" in skill_text def test_free_text_distinguishes_parameter_architecture_and_new_intent_changes(self, skill_text): assert "调整当前参数" in skill_text diff --git a/tests/pipeline/selling_solution_first/test_pipeline_definition.py b/tests/pipeline/selling_solution_first/test_pipeline_definition.py index 57cb74ae..906eb044 100644 --- a/tests/pipeline/selling_solution_first/test_pipeline_definition.py +++ b/tests/pipeline/selling_solution_first/test_pipeline_definition.py @@ -138,7 +138,9 @@ def test_candidate_and_deployment_options_keep_separate_schemas(self, raw_yaml): confirmation_options = raw_yaml["steps"][1]["conclusion_schema"]["properties"]["options"] assert confirmation_options["minItems"] == 2 assert confirmation_options["maxItems"] == 4 - required_actions = {rule["contains"]["properties"]["action"]["const"] for rule in confirmation_options["allOf"]} + required_actions = { + rule["contains"]["properties"]["action"]["const"] for rule in confirmation_options["allOf"] + } assert required_actions == {"confirm", "cancel"} assert set(confirmation_options["items"]["properties"]["action"]["enum"]) == { "confirm", @@ -159,10 +161,9 @@ def test_complete_step_schemas_describe_branch_identity_and_handoff_fields(self, candidate = planning_schema["properties"]["candidates"]["items"] assert "原样取自" in candidate["description"] assert "Step 2 唯一允许写入" in candidate["properties"]["output_path"]["description"] - assert ( - "0 基下标" - in planning_schema["properties"]["options"]["items"]["properties"]["candidate_index"]["description"] - ) + assert "0 基下标" in planning_schema["properties"]["options"]["items"]["properties"][ + "candidate_index" + ]["description"] assert "原样等于" in planning_schema["properties"]["selected_candidate"]["description"] assert materialize_schema is not None @@ -172,7 +173,9 @@ def test_complete_step_schemas_describe_branch_identity_and_handoff_fields(self, assert "面向最终用户" in materialized["solution_summary"]["description"] assert "ROS 精确询价" in materialized["cost"]["description"] assert "同一路径" in materialized["template"]["properties"]["file_path"]["description"] - assert "没有覆盖时必须使用空对象" in materialize_schema["properties"]["parameter_overrides"]["description"] + assert "没有覆盖时必须使用空对象" in materialize_schema["properties"]["parameter_overrides"][ + "description" + ] assert "真实用户确认输入" in materialize_schema["properties"]["confirmation"]["description"] # Step 3 继续从共享 deploying skill 继承 schema,不在新 pipeline 复制一份。 diff --git a/tests/pipeline/selling_solution_first/test_solution_planning_step.py b/tests/pipeline/selling_solution_first/test_solution_planning_step.py index 5479f037..d40534d1 100644 --- a/tests/pipeline/selling_solution_first/test_solution_planning_step.py +++ b/tests/pipeline/selling_solution_first/test_solution_planning_step.py @@ -334,7 +334,9 @@ async def test_step_two_emits_a_dedicated_confirmation_payload_and_waits_again_a first_resume = await _drain(runner.resume(encode_selected_candidate(CANDIDATES[1]["name"], 1))) confirmation_wait = next( - event for event in _input_required(first_resume) if event.step_id == "materialize_selected_candidate" + event + for event in _input_required(first_resume) + if event.step_id == "materialize_selected_candidate" ) assert confirmation_wait.data["kind"] == "deployment_confirmation" assert confirmation_wait.data["solution_summary"] == "SLB + 双 ECS + RDS 高可用方案" @@ -377,7 +379,11 @@ async def test_unchanged_structured_confirm_is_resolved_once_and_advances_to_dep events = await _drain(runner.resume('{"action":"confirm","parameter_overrides":{}}')) - assert not [event for event in _input_required(events) if event.step_id == "materialize_selected_candidate"] + assert not [ + event + for event in _input_required(events) + if event.step_id == "materialize_selected_candidate" + ] assert [call["step_id"] for call in executor.calls] == [ STEP_ID, STEP_ID, @@ -686,7 +692,9 @@ def prompt_text(self) -> str: @pytest.fixture(scope="class") def skill_text(self) -> str: - return (_pipeline_dir() / "skills" / "iac-aliyun-solution-first" / "SKILL.md").read_text(encoding="utf-8") + return ( + _pipeline_dir() / "skills" / "iac-aliyun-solution-first" / "SKILL.md" + ).read_text(encoding="utf-8") def test_conclusion_schema_covers_the_three_outcomes(self, raw_step): schema = raw_step["conclusion_schema"] @@ -751,15 +759,9 @@ def test_compact_completion_requires_authoritative_resource_lifecycle(self, raw_ assert set(intent["required"]) == {"resource_intents", "hard_constraints"} assert resource_intents["minItems"] == 1 - assert all( - action in resource_intents["description"] - for action in ( - "create", - "use_existing", - "reference", - "forbid", - ) - ) + assert all(action in resource_intents["description"] for action in ( + "create", "use_existing", "reference", "forbid", + )) assert "ECS:forbid" in resource_intents["description"] def test_options_require_the_candidate_index_coordinate(self, raw_step): diff --git a/tests/pipeline_e2e/test_selling_solution_first_run_scenarios.py b/tests/pipeline_e2e/test_selling_solution_first_run_scenarios.py index 04d3588f..a71fe521 100644 --- a/tests/pipeline_e2e/test_selling_solution_first_run_scenarios.py +++ b/tests/pipeline_e2e/test_selling_solution_first_run_scenarios.py @@ -600,7 +600,9 @@ def _json_request(_base_url: str, _method: str, _path: str) -> dict[str, object] def test_web_confirmation_boundary_accepts_repeated_parameter_questions(runner: ModuleType) -> None: for kind in ("ask_user_question", "deployment_confirmation"): - assert runner._web_at_confirmation_boundary({"pipeline": {"snapshot": {"pendingInput": {"kind": kind}}}}) + assert runner._web_at_confirmation_boundary( + {"pipeline": {"snapshot": {"pendingInput": {"kind": kind}}}} + ) assert not runner._web_at_confirmation_boundary( {"pipeline": {"snapshot": {"pendingInput": {"kind": "candidate_selection"}}}} @@ -609,7 +611,9 @@ def test_web_confirmation_boundary_accepts_repeated_parameter_questions(runner: def test_web_materialize_boundary_fails_fast_on_unexpected_rollback(runner: ModuleType) -> None: for kind in ("ask_user_question", "deployment_confirmation", "candidate_selection", "candidate_select"): - assert runner._web_at_materialize_boundary({"pipeline": {"snapshot": {"pendingInput": {"kind": kind}}}}) + assert runner._web_at_materialize_boundary( + {"pipeline": {"snapshot": {"pendingInput": {"kind": kind}}}} + ) def test_w02_parameter_answer_preserves_create_goal(runner: ModuleType) -> None: @@ -801,7 +805,9 @@ def test_web_session_uses_valid_unattended_permission_mode(runner: ModuleType, t assert payload["mode"] == "pipeline" -def test_browser_dependency_preflight_reports_missing_node(runner: ModuleType, monkeypatch: pytest.MonkeyPatch) -> None: +def test_browser_dependency_preflight_reports_missing_node( + runner: ModuleType, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr(runner.shutil, "which", lambda _name: None) result = runner._run_browser_dependency_preflight(timeout=1.0) @@ -1511,7 +1517,9 @@ def test_desktop_result_requires_the_full_native_contract(runner: ModuleType) -> assert not all(runner.validate_desktop_result(result).values()) -def test_desktop_source_resource_audit_follows_linked_reference_directory(runner: ModuleType, tmp_path: Path) -> None: +def test_desktop_source_resource_audit_follows_linked_reference_directory( + runner: ModuleType, tmp_path: Path +) -> None: source_root = tmp_path / "pipeline" shared_references = tmp_path / "shared-references" linked_references = source_root / "skills" / "materialize" / "references" diff --git a/tests/providers/test_dashscope_provider.py b/tests/providers/test_dashscope_provider.py index ede49bf7..41f6ebea 100644 --- a/tests/providers/test_dashscope_provider.py +++ b/tests/providers/test_dashscope_provider.py @@ -103,11 +103,15 @@ def test_kimi_k3_preserves_thinking_without_enable_flag(self): def test_bailian_hosted_kimi_k3_keeps_always_on_thinking(self): p = DashScopeProvider(model="kimi-k3", api_key="k", thinking_enabled=False) - assert p._build_thinking_kwargs() == {"extra_body": {"enable_thinking": True, "preserve_thinking": True}} + assert p._build_thinking_kwargs() == { + "extra_body": {"enable_thinking": True, "preserve_thinking": True} + } def test_qwen38_open_model_supports_thinking_budget(self): p = DashScopeProvider(model="qwen3.8-2.4t-a95b", api_key="k", thinking_budget=2048) - assert p._build_thinking_kwargs() == {"extra_body": {"enable_thinking": True, "thinking_budget": 2048}} + assert p._build_thinking_kwargs() == { + "extra_body": {"enable_thinking": True, "thinking_budget": 2048} + } def test_stepfun_uses_its_documented_effort_values(self): p = DashScopeProvider(model="stepfun/step-3.7-flash", api_key="k", effort="medium") diff --git a/tests/providers/test_provider_model_research_updates.py b/tests/providers/test_provider_model_research_updates.py index 7c6968f7..25e96608 100644 --- a/tests/providers/test_provider_model_research_updates.py +++ b/tests/providers/test_provider_model_research_updates.py @@ -443,7 +443,9 @@ def test_dashscope_new_model_protocols_are_not_flattened() -> None: model="kimi-k3", api_key="k", thinking_enabled=False, - )._build_thinking_kwargs() == {"extra_body": {"enable_thinking": True, "preserve_thinking": True}} + )._build_thinking_kwargs() == { + "extra_body": {"enable_thinking": True, "preserve_thinking": True} + } stepfun = get_thinking_spec("dashscope", "stepfun/step-3.7-flash") assert stepfun.allowed_efforts == (EffortLevel.LOW, EffortLevel.MEDIUM, EffortLevel.HIGH) diff --git a/tests/providers/test_qwen_provider.py b/tests/providers/test_qwen_provider.py index 7f9fe358..f899b43e 100644 --- a/tests/providers/test_qwen_provider.py +++ b/tests/providers/test_qwen_provider.py @@ -770,7 +770,9 @@ async def test_terminal_attribution_separates_logical_wire_service_adapter_and_m provider_key_override="openai_compatible", ) events = [event async for event in manager.stream([Message.user("hi")], "base")] - attribution = next(event.usage_attribution for event in events if isinstance(event, MessageEndEvent)) + attribution = next( + event.usage_attribution for event in events if isinstance(event, MessageEndEvent) + ) assert ( attribution.logical_provider_key, attribution.wire_provider_key, @@ -903,7 +905,9 @@ async def test_unsafe_stream_successful_replay_keeps_terminal_attribution(self, @pytest.mark.asyncio class TestQwenResponses: async def test_streaming_and_non_streaming_cache_marker_scope(self): - stream_client = FakeOpenAIClient(stream_chunks=[_chunk(content="ok"), _chunk(finish_reason="stop")]) + stream_client = FakeOpenAIClient( + stream_chunks=[_chunk(content="ok"), _chunk(finish_reason="stop")] + ) stream_provider = QwenProvider(model="qwen3.7-plus", client=stream_client) _ = [ event @@ -938,8 +942,14 @@ async def test_streaming_and_non_streaming_cache_marker_scope(self): @pytest.mark.parametrize("streaming", [False, True]) async def test_required_thinking_error_rebuilds_once_and_is_learned(self, streaming): - error = _RequiredThinkingError("The value of the enable_thinking parameter is restricted to True.") - success = [_chunk(content="ok"), _chunk(finish_reason="stop")] if streaming else _response(content="ok") + error = _RequiredThinkingError( + "The value of the enable_thinking parameter is restricted to True." + ) + success = ( + [_chunk(content="ok"), _chunk(finish_reason="stop")] + if streaming + else _response(content="ok") + ) client, completions = _sequential_client(error, success, success) provider = QwenProvider( model="qwen3.8-max", @@ -964,7 +974,9 @@ async def test_required_thinking_error_rebuilds_once_and_is_learned(self, stream assert completions.calls[2]["extra_body"]["enable_thinking"] is True async def test_required_thinking_retry_reports_both_real_api_attempts(self, monkeypatch): - error = _RequiredThinkingError("The value of the enable_thinking parameter is restricted to True.") + error = _RequiredThinkingError( + "The value of the enable_thinking parameter is restricted to True." + ) client, completions = _sequential_client( error, [_chunk(content="ok"), _chunk(finish_reason="stop")], @@ -1019,7 +1031,9 @@ async def test_required_thinking_retry_reports_both_real_api_attempts(self, monk assert all(attrs[IacCodeAttr.PROVIDER_ADAPTER] == "qwen" for attrs in request_metrics) async def test_mandatory_learning_does_not_rewrite_an_existing_stream_context(self): - error = _RequiredThinkingError("The value of the enable_thinking parameter is restricted to True.") + error = _RequiredThinkingError( + "The value of the enable_thinking parameter is restricted to True." + ) client, completions = _sequential_client( error, _response(content="learned"), @@ -1119,12 +1133,16 @@ async def test_reasoning_content_wins_once_and_empty_blocks_fallback(self): client = FakeOpenAIClient( create_response=_response(content="answer", reasoning_content="", reasoning="must-not-appear") ) - response = await QwenProvider(model="qwen3.7-plus", client=client).complete([Message.user("hi")], "sys") + response = await QwenProvider(model="qwen3.7-plus", client=client).complete( + [Message.user("hi")], "sys" + ) assert response.thinking == "" assert response.thinking_blocks == [] async def test_unclosed_thinking_tag_is_unsafe(self): - client = FakeOpenAIClient(stream_chunks=[_chunk(content="secret"), _chunk(finish_reason="stop")]) + client = FakeOpenAIClient( + stream_chunks=[_chunk(content="secret"), _chunk(finish_reason="stop")] + ) provider = QwenProvider(model="qwen3.7-plus", client=client) with pytest.raises(UnsafeStreamProtocolError): _ = [event async for event in provider.stream([Message.user("hi")], "sys")] @@ -1133,7 +1151,9 @@ async def test_user_visible_stream_protocol_error_uses_runtime_translation(self, import iac_code.providers.streaming as streaming monkeypatch.setattr(streaming, "_", lambda message: f"translated:{message}") - client = FakeOpenAIClient(stream_chunks=[_chunk(content="secret"), _chunk(finish_reason="stop")]) + client = FakeOpenAIClient( + stream_chunks=[_chunk(content="secret"), _chunk(finish_reason="stop")] + ) provider = QwenProvider(model="qwen3.7-plus", client=client) with pytest.raises(UnsafeStreamProtocolError, match=r"^translated:Qwen emitted"): _ = [event async for event in provider.stream([Message.user("hi")], "sys")] @@ -1144,7 +1164,9 @@ async def test_tag_probe_literal_conflict_and_balanced_sequences(self): ) events = [ event - async for event in QwenProvider(model="qwen3.7-plus", client=literal).stream([Message.user("hi")], "sys") + async for event in QwenProvider(model="qwen3.7-plus", client=literal).stream( + [Message.user("hi")], "sys" + ) ] assert "".join(event.text for event in events if isinstance(event, TextDeltaEvent)) == "a.py' + 'a.py' + "" ) fallback = FakeOpenAIClient(stream_chunks=[_chunk(content=xml), _chunk(finish_reason="stop")]) provider = QwenProvider(model="qwen3.7-plus", client=fallback) @@ -1250,7 +1277,9 @@ async def test_strict_native_tool_call_and_xml_fallback(self): async def test_real_dashscope_empty_tool_delimiters_do_not_create_anonymous_call(self): client = FakeOpenAIClient( stream_chunks=[ - _chunk(tool_calls=[ns(index=0, id="call_1", function=ns(name="read_file", arguments=""))]), + _chunk( + tool_calls=[ns(index=0, id="call_1", function=ns(name="read_file", arguments=""))] + ), _chunk(tool_calls=[ns(index=0, id="", function=ns(name=None, arguments=""))]), _chunk(tool_calls=[ns(index=0, id="", function=ns(name=None, arguments='{"path": '))]), _chunk(tool_calls=[ns(index=0, id="", function=ns(name=None, arguments='"a.py"}'))]), @@ -1264,7 +1293,9 @@ async def test_real_dashscope_empty_tool_delimiters_do_not_create_anonymous_call [Message.user("hi")], "sys", [_tool()] ) ] - assert [event.input for event in events if isinstance(event, ToolUseEndEvent)] == [{"path": "a.py"}] + assert [event.input for event in events if isinstance(event, ToolUseEndEvent)] == [ + {"path": "a.py"} + ] assert next(event for event in events if isinstance(event, MessageEndEvent)).stop_reason == "tool_use" async def test_malformed_native_call_does_not_fall_through_to_xml(self): @@ -1301,7 +1332,9 @@ async def test_non_streaming_reasoning_and_xml(self): ], ) async def test_markdown_quoted_xml_is_never_executed(self, quoted): - stream_client = FakeOpenAIClient(stream_chunks=[_chunk(content=quoted), _chunk(finish_reason="stop")]) + stream_client = FakeOpenAIClient( + stream_chunks=[_chunk(content=quoted), _chunk(finish_reason="stop")] + ) events = [ event async for event in QwenProvider(model="qwen3.7-plus", client=stream_client).stream( @@ -1358,7 +1391,9 @@ async def test_native_tool_call_wins_over_xml_and_missing_finish_does_not_recove ) ] assert "".join(event.text for event in events if isinstance(event, TextDeltaEvent)) == xml - assert [event.input for event in events if isinstance(event, ToolUseEndEvent)] == [{"path": "native.py"}] + assert [event.input for event in events if isinstance(event, ToolUseEndEvent)] == [ + {"path": "native.py"} + ] no_finish = FakeOpenAIClient(stream_chunks=[_chunk(content=xml)]) events = [ diff --git a/tests/providers/test_qwen_tool_call_parser.py b/tests/providers/test_qwen_tool_call_parser.py index 7427b3a7..3dc0de36 100644 --- a/tests/providers/test_qwen_tool_call_parser.py +++ b/tests/providers/test_qwen_tool_call_parser.py @@ -137,7 +137,8 @@ def test_xml_requires_registered_parameterized_invoke_and_intent_guard(): def test_xml_rejects_duplicate_or_nested_parameters_and_preserves_scalar_strings(): duplicate = ( - 'ab' + 'a' + 'b' ) nested = 'a' assert recover_xml_tool_calls(duplicate, _tools()) is None @@ -156,13 +157,14 @@ def test_xml_multiple_calls_wrapper_entities_newlines_and_unicode(): recovery = recover_xml_tool_calls(text, tools) assert recovery.remaining_text == "" assert recovery.calls[0]["input"] == {"path": "目录/a.py"} - assert recovery.calls[1]["input"] == {"content": "&\"'", "meta": {"ok": True}} + assert recovery.calls[1]["input"] == {"content": '&"\'', "meta": {"ok": True}} @pytest.mark.parametrize( "text", [ - 'texta', + 'texta' + "", 'a', 'a', '~~~xml\na\n~~~', diff --git a/tests/skill_bridge/test_alicloud_ros_agent_bridge.py b/tests/skill_bridge/test_alicloud_ros_agent_bridge.py index a1b9b42e..9b7c778e 100644 --- a/tests/skill_bridge/test_alicloud_ros_agent_bridge.py +++ b/tests/skill_bridge/test_alicloud_ros_agent_bridge.py @@ -1158,7 +1158,7 @@ def test_cli_plugin_parser_accepts_pretty_printed_objects_and_arrays() -> None: def test_cli_plugin_parser_reports_an_unterminated_buffer_as_malformed() -> None: - events = list(bridge.iter_cli_plugin_payloads(["{\n", ' "data": {\n'])) + events = list(bridge.iter_cli_plugin_payloads(['{\n', ' "data": {\n'])) assert events == [(None, '{\n "data": {')] diff --git a/tests/skill_bridge/test_runtime_release.py b/tests/skill_bridge/test_runtime_release.py index 27a9efd4..9ad3da97 100644 --- a/tests/skill_bridge/test_runtime_release.py +++ b/tests/skill_bridge/test_runtime_release.py @@ -107,7 +107,9 @@ def test_runtime_archive_and_version_marker_are_rooted_consistently(tmp_path: Pa assert "artifactRevision" not in marker -def test_runtime_a2a_smoke_checks_health_and_agent_card(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_runtime_a2a_smoke_checks_health_and_agent_card( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: module = _load_module("skill_runtime_smoke", BUILD_SCRIPT) server = tmp_path / "server.py" server.write_text( diff --git a/tests/tools/cloud/aliyun/test_ros_validate_hook.py b/tests/tools/cloud/aliyun/test_ros_validate_hook.py index 259f1190..e0744b8e 100644 --- a/tests/tools/cloud/aliyun/test_ros_validate_hook.py +++ b/tests/tools/cloud/aliyun/test_ros_validate_hook.py @@ -446,7 +446,9 @@ def test_eip_bound_instance_group_detects_select_getatt_reference(self) -> None: "Type": "ALIYUN::VPC::EIPAssociation", "Properties": { "AllocationId": {"Ref": "Eip"}, - "InstanceId": {"Fn::Select": [0, {"Fn::GetAtt": ["InstanceGroup", "InstanceIds"]}]}, + "InstanceId": { + "Fn::Select": [0, {"Fn::GetAtt": ["InstanceGroup", "InstanceIds"]}] + }, }, }, }, diff --git a/tests/web/test_diagrams.py b/tests/web/test_diagrams.py index 297f8f5d..edd0a5ea 100644 --- a/tests/web/test_diagrams.py +++ b/tests/web/test_diagrams.py @@ -255,7 +255,9 @@ def test_outputs_payload_includes_solution_first_step1_plan_and_rough_cost(tmp_p assert item["diagramStage"] == "optimized" assert item["optimized"] is True assert item["totalMonthlyCost"] == "约 ¥300/月(架构粗估)" - assert item["costItems"] == [{"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthly_cost": "约 ¥300/月"}] + assert item["costItems"] == [ + {"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthly_cost": "约 ¥300/月"} + ] assert item["views"] == [ { "id": "overview", @@ -290,7 +292,9 @@ def test_diagram_items_keeps_latest_step1_plan_and_step2_template(tmp_path): def test_diagram_items_ignores_unmarked_template_less_diagram(tmp_path): - manager = _Manager([_architecture_plan_envelope(0, "非规划图", "flowchart TD\n A", marked=False)]) + manager = _Manager( + [_architecture_plan_envelope(0, "非规划图", "flowchart TD\n A", marked=False)] + ) assert diagram_items(manager, _session(tmp_path)) == [] @@ -322,7 +326,9 @@ def test_solution_first_materialized_diagram_uses_exact_quote_and_optimized_cach assert item["mermaidSource"] == "graph TD\n OPT[优化图]" assert item["views"][0]["title"] == "优化总览" assert item["totalMonthlyCost"] == "¥88/月" - assert item["costItems"] == [{"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthly_cost": "¥88/月"}] + assert item["costItems"] == [ + {"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthly_cost": "¥88/月"} + ] def test_diagram_items_dedupes_solution_first_absolute_and_relative_template_paths(tmp_path): diff --git a/tests/web/test_frontend_static.py b/tests/web/test_frontend_static.py index b7e3e3a6..4a8cc77c 100644 --- a/tests/web/test_frontend_static.py +++ b/tests/web/test_frontend_static.py @@ -2559,7 +2559,7 @@ def test_completed_turn_collapses_process_into_summary() -> None: # 「已处理」组的展开态必须跨重建保留:openKey 让 toggle 记录器登记用户操作、 # applyDetailsOpenOverrides 在重建后恢复;键取 turnId,缺 turnId 时回退首条消息 id。 assert 'const turnKey = turnId || text(agentMessages[0]?.messageId || agentMessages[0]?.id || "");' in app_source - assert "details.dataset.openKey = `turnproc:${turnKey}`;" in app_source + assert 'details.dataset.openKey = `turnproc:${turnKey}`;' in app_source # 只有最后一次工具调用之后的文本才是「最终回答」;此前每个步骤的文本旁白 # (夹在工具调用之间的 text delta)连同思考、工具一起折进「已处理」,不平铺成答案。 diff --git a/tests/web/test_pipeline_transcript.py b/tests/web/test_pipeline_transcript.py index 768c0ead..a610783c 100644 --- a/tests/web/test_pipeline_transcript.py +++ b/tests/web/test_pipeline_transcript.py @@ -1119,7 +1119,8 @@ def test_repeated_step_completions_accumulate_processing_duration(): completed_markers = [ event for event in events - if event["type"] == PIPELINE_MARKER_EVENT and event["payload"]["pipelineStep"]["status"] == "completed" + if event["type"] == PIPELINE_MARKER_EVENT + and event["payload"]["pipelineStep"]["status"] == "completed" ] assert completed_markers[-1]["payload"]["pipelineStep"]["durationS"] == 135.44 diff --git a/tests/web/test_session_manager.py b/tests/web/test_session_manager.py index 68ced3c2..b953e252 100644 --- a/tests/web/test_session_manager.py +++ b/tests/web/test_session_manager.py @@ -114,7 +114,9 @@ def test_runtime_settings_payload_redacts_only_editable_cloud_credentials(monkey ("cancel", "Cancel"), ), ) -def test_solution_first_pipeline_user_display_text_hides_structured_control_json(action: str, expected: str) -> None: +def test_solution_first_pipeline_user_display_text_hides_structured_control_json( + action: str, expected: str +) -> None: raw = json.dumps({"action": action, "parameter_overrides": {"ZoneId": "cn-hangzhou-i"}}) assert solution_first_pipeline_user_display_text("selling_solution_first", raw) == expected