diff --git a/src/iac_code/a2a/executor.py b/src/iac_code/a2a/executor.py index 226a2d44..8c642210 100644 --- a/src/iac_code/a2a/executor.py +++ b/src/iac_code/a2a/executor.py @@ -1731,9 +1731,7 @@ def _resolve_cwd(self, metadata: Any | None) -> str: raise ValueError("Invalid A2A workspace metadata.") logical_cwd = os.path.normpath(cwd) resolved_cwd = resolve_workspace_path(Path(logical_cwd)) - if not trust_request_cwd() and not any( - _is_relative_to(resolved_cwd, root) for root in _allowed_cwd_roots() - ): + if not trust_request_cwd() and not any(_is_relative_to(resolved_cwd, root) for root in _allowed_cwd_roots()): raise ValueError("Invalid A2A workspace metadata.") if resolved_cwd.exists(): if not resolved_cwd.is_dir(): diff --git a/src/iac_code/a2a/input_required.py b/src/iac_code/a2a/input_required.py index 4418de08..c0ba54a3 100644 --- a/src/iac_code/a2a/input_required.py +++ b/src/iac_code/a2a/input_required.py @@ -178,35 +178,29 @@ def permission_display_fields(request: PermissionRequestEvent, *, language: str command = safe_input.get("command") or safe_input.get("cmd") if isinstance(command, str) and command.strip(): command_fallback = translate_message("shell command", language=language) - target = translate_message( - "the current local workspace; command: {command}", language=language - ).format(command=_display_text(command, fallback=command_fallback, maximum=240)) + target = translate_message("the current local workspace; command: {command}", language=language).format( + command=_display_text(command, fallback=command_fallback, maximum=240) + ) else: target = translate_message("the current local workspace", language=language) effect = "read" if is_read_only else ("local_execution" if read_only_known else "unknown") elif tool_name in {"write_file", "edit_file"}: title = translate_message("Change a workspace file", language=language) - purpose = translate_message( - "Write a file needed for the requested infrastructure task.", language=language - ) + purpose = translate_message("Write a file needed for the requested infrastructure task.", language=language) target = _safe_input_target(safe_input, language=language) or translate_message( "a file in the current workspace", language=language ) effect = "file_change" elif tool_name in {"read_file", "glob", "grep"} or is_read_only: title = translate_message("Read workspace data with {tool}", language=language).format(tool=public_tool) - purpose = translate_message( - "Read local data needed for the requested infrastructure task.", language=language - ) + purpose = translate_message("Read local data needed for the requested infrastructure task.", language=language) target = _safe_input_target(safe_input, language=language) or translate_message( "the current local workspace", language=language ) effect = "read" else: title = translate_message("Run {tool}", language=language).format(tool=public_tool) - purpose = translate_message( - "Run this operation for the requested infrastructure task.", language=language - ) + purpose = translate_message("Run this operation for the requested infrastructure task.", language=language) target = _safe_input_target(safe_input, language=language) or translate_message( "the current task workspace or cloud account", language=language ) @@ -319,9 +313,7 @@ def _cloud_operation_title(product: str, action: str, *, is_read_only: bool, lan if action == "CreateStack": return translate_message("Create {product} stack", language=language).format(product=product_label) if action == "ContinueCreateStack": - return translate_message("Continue creating {product} stack", language=language).format( - product=product_label - ) + return translate_message("Continue creating {product} stack", language=language).format(product=product_label) if action == "UpdateStack": return translate_message("Update {product} stack", language=language).format(product=product_label) if action == "DeleteStack": @@ -344,9 +336,7 @@ def _safe_input_target(value: Any, *, language: str) -> str: for key in ("file_path", "filePath", "path", "region_id", "regionId", "resource_id", "resourceId"): candidate = value.get(key) if isinstance(candidate, str) and candidate.strip(): - return _display_text( - candidate, fallback=translate_message("the current task scope", language=language) - ) + return _display_text(candidate, fallback=translate_message("the current task scope", language=language)) return "" diff --git a/src/iac_code/a2a/pipeline_executor.py b/src/iac_code/a2a/pipeline_executor.py index cf073fec..127cb079 100644 --- a/src/iac_code/a2a/pipeline_executor.py +++ b/src/iac_code/a2a/pipeline_executor.py @@ -1235,11 +1235,7 @@ def _create_pipeline( def permission_context_getter() -> Any: return getattr(agent_loop, "_permission_context", None) - surface = ( - A2A_RICH_CANDIDATE_SURFACE - if self._candidate_presentation == RICH_CANDIDATE_PRESENTATION - else "a2a" - ) + surface = A2A_RICH_CANDIDATE_SURFACE if self._candidate_presentation == RICH_CANDIDATE_PRESENTATION else "a2a" return create_pipeline( pipeline_name, provider_manager=runtime.provider_manager, diff --git a/src/iac_code/a2a/task_store.py b/src/iac_code/a2a/task_store.py index 0f1ae69d..a0906315 100644 --- a/src/iac_code/a2a/task_store.py +++ b/src/iac_code/a2a/task_store.py @@ -864,9 +864,7 @@ async def has_active_work(self) -> bool: any(record.active_task is not None and not record.active_task.done() for record in self._tasks.values()) or any(not task.done() for task in self._context_runtime_tasks.values()) or any( - not task.done() - for starts in self._context_execution_starts.values() - for task in starts.values() + not task.done() for starts in self._context_execution_starts.values() for task in starts.values() ) or any(self._context_reconciliation_waiters.values()) or any(lock.locked() for lock in self._reconciliation_locks.values()) 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 e00a5449..39668e99 100644 --- a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po @@ -4086,6 +4086,16 @@ msgstr "" "eine erfolgreiche Prüfung mit übereinstimmenden Parametern und Nachweisen" " abgedeckt sein." +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "" +"The final instance spec must match the architecture plan, budget " +"deviation must be disclosed, and a contract discount must only be " +"reported when it actually reduces the price." +msgstr "" +"Die endgültige Instanzspezifikation muss dem Architekturplan entsprechen," +" Budgetabweichungen müssen offengelegt werden, und ein Vertragsrabatt " +"darf nur ausgewiesen werden, wenn er den Preis tatsächlich senkt." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "" "Complete the current step by calling this tool to submit the conclusion. " @@ -4194,15 +4204,15 @@ msgstr "" "{message} complete_step.conclusion muss eines dieser Felder enthalten: " "{fields}." -#: src/iac_code/pipeline/engine/complete_step_tool.py -msgid "A completion guard is misconfigured." -msgstr "Eine Abschlussbedingung ist falsch konfiguriert." - #: src/iac_code/pipeline/engine/complete_step_tool.py #, python-brace-format msgid "{message} Validation issue: {code} ({detail})." msgstr "{message} Validierungsproblem: {code} ({detail})." +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "A completion guard is misconfigured." +msgstr "Eine Abschlussbedingung ist falsch konfiguriert." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "A conclusion content hash is required before completing the current step." msgstr "" diff --git a/src/iac_code/i18n/locales/de/LC_MESSAGES/webui.po b/src/iac_code/i18n/locales/de/LC_MESSAGES/webui.po index 8bdf883e..c382a8b8 100644 --- a/src/iac_code/i18n/locales/de/LC_MESSAGES/webui.po +++ b/src/iac_code/i18n/locales/de/LC_MESSAGES/webui.po @@ -545,6 +545,14 @@ msgstr "{n}Wo" msgid "{n}y" msgstr "{n}J" +#: src/iac_code/web/static/js/app.js +msgid "Operation failed" +msgstr "Vorgang fehlgeschlagen" + +#: src/iac_code/web/static/js/app.js +msgid "Archive failed" +msgstr "Archivierung fehlgeschlagen" + #: src/iac_code/web/static/js/app.js msgid "Read-only" msgstr "Schreibgeschützt" @@ -561,14 +569,6 @@ msgstr "Bitte geben Sie einen Inhalt ein" msgid "Please enter a name" msgstr "Bitte geben Sie einen Namen ein" -#: src/iac_code/web/static/js/app.js -msgid "Operation failed" -msgstr "Vorgang fehlgeschlagen" - -#: src/iac_code/web/static/js/app.js -msgid "Archive failed" -msgstr "Archivierung fehlgeschlagen" - #: src/iac_code/web/static/js/app.js #, python-brace-format msgid "Remove {label}?" 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 be131234..3241cb84 100644 --- a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po @@ -4063,6 +4063,17 @@ msgstr "" "Cada restricción estricta explícita del usuario debe estar cubierta por " "una comprobación satisfactoria con parámetros y evidencias coincidentes." +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "" +"The final instance spec must match the architecture plan, budget " +"deviation must be disclosed, and a contract discount must only be " +"reported when it actually reduces the price." +msgstr "" +"La especificación final de la instancia debe coincidir con el plan de " +"arquitectura, se debe divulgar la desviación del presupuesto y un " +"descuento contractual solo se puede informar cuando realmente reduce el " +"precio." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "" "Complete the current step by calling this tool to submit the conclusion. " @@ -4168,15 +4179,15 @@ msgstr "" "{message} complete_step.conclusion debe incluir uno de estos campos: " "{fields}." -#: src/iac_code/pipeline/engine/complete_step_tool.py -msgid "A completion guard is misconfigured." -msgstr "La guarda de finalización está mal configurada." - #: src/iac_code/pipeline/engine/complete_step_tool.py #, python-brace-format msgid "{message} Validation issue: {code} ({detail})." msgstr "{message} Problema de validación: {code} ({detail})." +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "A completion guard is misconfigured." +msgstr "La guarda de finalización está mal configurada." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "A conclusion content hash is required before completing the current step." msgstr "" diff --git a/src/iac_code/i18n/locales/es/LC_MESSAGES/webui.po b/src/iac_code/i18n/locales/es/LC_MESSAGES/webui.po index 567f2626..945258ef 100644 --- a/src/iac_code/i18n/locales/es/LC_MESSAGES/webui.po +++ b/src/iac_code/i18n/locales/es/LC_MESSAGES/webui.po @@ -547,6 +547,14 @@ msgstr "{n}sem" msgid "{n}y" msgstr "{n}a" +#: src/iac_code/web/static/js/app.js +msgid "Operation failed" +msgstr "La operación falló" + +#: src/iac_code/web/static/js/app.js +msgid "Archive failed" +msgstr "Error al archivar" + #: src/iac_code/web/static/js/app.js msgid "Read-only" msgstr "Solo lectura" @@ -563,14 +571,6 @@ msgstr "Introduce contenido" msgid "Please enter a name" msgstr "Introduce un nombre" -#: src/iac_code/web/static/js/app.js -msgid "Operation failed" -msgstr "La operación falló" - -#: src/iac_code/web/static/js/app.js -msgid "Archive failed" -msgstr "Error al archivar" - #: src/iac_code/web/static/js/app.js #, python-brace-format msgid "Remove {label}?" 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 67ef1a4c..3258d9a4 100644 --- a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po @@ -4072,6 +4072,17 @@ msgstr "" "être couverte par une vérification réussie avec des paramètres et des " "preuves concordants." +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "" +"The final instance spec must match the architecture plan, budget " +"deviation must be disclosed, and a contract discount must only be " +"reported when it actually reduces the price." +msgstr "" +"La spécification finale de l’instance doit correspondre au plan " +"d’architecture, tout écart budgétaire doit être signalé, et une remise " +"contractuelle ne peut être mentionnée que si elle réduit réellement le " +"prix." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "" "Complete the current step by calling this tool to submit the conclusion. " @@ -4175,15 +4186,15 @@ msgstr "" "{message} complete_step.conclusion doit inclure l’un de ces champs : " "{fields}." -#: src/iac_code/pipeline/engine/complete_step_tool.py -msgid "A completion guard is misconfigured." -msgstr "Une garde de finalisation est mal configurée." - #: src/iac_code/pipeline/engine/complete_step_tool.py #, python-brace-format msgid "{message} Validation issue: {code} ({detail})." msgstr "{message} Problème de validation : {code} ({detail})." +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "A completion guard is misconfigured." +msgstr "Une garde de finalisation est mal configurée." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "A conclusion content hash is required before completing the current step." msgstr "" diff --git a/src/iac_code/i18n/locales/fr/LC_MESSAGES/webui.po b/src/iac_code/i18n/locales/fr/LC_MESSAGES/webui.po index 3e48caa8..0fd779ca 100644 --- a/src/iac_code/i18n/locales/fr/LC_MESSAGES/webui.po +++ b/src/iac_code/i18n/locales/fr/LC_MESSAGES/webui.po @@ -545,6 +545,14 @@ msgstr "{n} sem" msgid "{n}y" msgstr "{n} an" +#: src/iac_code/web/static/js/app.js +msgid "Operation failed" +msgstr "Échec de l’opération" + +#: src/iac_code/web/static/js/app.js +msgid "Archive failed" +msgstr "Échec de l’archivage" + #: src/iac_code/web/static/js/app.js msgid "Read-only" msgstr "Lecture seule" @@ -561,14 +569,6 @@ msgstr "Veuillez saisir du contenu" msgid "Please enter a name" msgstr "Veuillez saisir un nom" -#: src/iac_code/web/static/js/app.js -msgid "Operation failed" -msgstr "Échec de l’opération" - -#: src/iac_code/web/static/js/app.js -msgid "Archive failed" -msgstr "Échec de l’archivage" - #: src/iac_code/web/static/js/app.js #, python-brace-format msgid "Remove {label}?" 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 83869fb5..f4e2ae35 100644 --- a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po @@ -3874,6 +3874,13 @@ msgid "" "with matching parameters and evidence." msgstr "ユーザーが明示した各ハード制約は、パラメーターと根拠が一致する「満たしている」チェックで網羅する必要があります。" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "" +"The final instance spec must match the architecture plan, budget " +"deviation must be disclosed, and a contract discount must only be " +"reported when it actually reduces the price." +msgstr "最終的なインスタンス仕様はアーキテクチャ計画と一致していなければならず、予算の乖離は明示的に開示し、契約割引は実際に価格が下がる場合にのみ記載してください。" + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "" "Complete the current step by calling this tool to submit the conclusion. " @@ -3967,15 +3974,15 @@ msgid "" "{fields}." msgstr "{message} complete_step.conclusion には次のいずれかのフィールドが必要です: {fields}。" -#: src/iac_code/pipeline/engine/complete_step_tool.py -msgid "A completion guard is misconfigured." -msgstr "完了ガードの設定に誤りがあります。" - #: src/iac_code/pipeline/engine/complete_step_tool.py #, python-brace-format msgid "{message} Validation issue: {code} ({detail})." msgstr "{message} 検証上の問題: {code} ({detail})。" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "A completion guard is misconfigured." +msgstr "完了ガードの設定に誤りがあります。" + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "A conclusion content hash is required before completing the current step." msgstr "現在のステップを完了する前に、結論内容のハッシュが必要です。" diff --git a/src/iac_code/i18n/locales/ja/LC_MESSAGES/webui.po b/src/iac_code/i18n/locales/ja/LC_MESSAGES/webui.po index 3a9abeb3..292d76cc 100644 --- a/src/iac_code/i18n/locales/ja/LC_MESSAGES/webui.po +++ b/src/iac_code/i18n/locales/ja/LC_MESSAGES/webui.po @@ -539,6 +539,14 @@ msgstr "{n}週間" msgid "{n}y" msgstr "{n}年" +#: src/iac_code/web/static/js/app.js +msgid "Operation failed" +msgstr "操作に失敗しました" + +#: src/iac_code/web/static/js/app.js +msgid "Archive failed" +msgstr "アーカイブに失敗しました" + #: src/iac_code/web/static/js/app.js msgid "Read-only" msgstr "読み取り専用" @@ -555,14 +563,6 @@ msgstr "内容を入力してください" msgid "Please enter a name" msgstr "名前を入力してください" -#: src/iac_code/web/static/js/app.js -msgid "Operation failed" -msgstr "操作に失敗しました" - -#: src/iac_code/web/static/js/app.js -msgid "Archive failed" -msgstr "アーカイブに失敗しました" - #: src/iac_code/web/static/js/app.js #, python-brace-format msgid "Remove {label}?" 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 953c6a8e..a677b0c5 100644 --- a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po @@ -4044,6 +4044,16 @@ msgstr "" "Cada restrição rígida explícita do usuário deve ser coberta por uma " "verificação satisfatória com parâmetros e evidências correspondentes." +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "" +"The final instance spec must match the architecture plan, budget " +"deviation must be disclosed, and a contract discount must only be " +"reported when it actually reduces the price." +msgstr "" +"A especificação final da instância deve corresponder ao plano de " +"arquitetura, o desvio de orçamento deve ser divulgado e um desconto " +"contratual só pode ser informado quando realmente reduz o preço." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "" "Complete the current step by calling this tool to submit the conclusion. " @@ -4144,15 +4154,15 @@ msgstr "" "{message} complete_step.conclusion deve incluir um destes campos: " "{fields}." -#: src/iac_code/pipeline/engine/complete_step_tool.py -msgid "A completion guard is misconfigured." -msgstr "Uma proteção de conclusão está configurada incorretamente." - #: src/iac_code/pipeline/engine/complete_step_tool.py #, python-brace-format msgid "{message} Validation issue: {code} ({detail})." msgstr "{message} Problema de validação: {code} ({detail})." +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "A completion guard is misconfigured." +msgstr "Uma proteção de conclusão está configurada incorretamente." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "A conclusion content hash is required before completing the current step." msgstr "" diff --git a/src/iac_code/i18n/locales/pt/LC_MESSAGES/webui.po b/src/iac_code/i18n/locales/pt/LC_MESSAGES/webui.po index d3e33fd4..4fa71155 100644 --- a/src/iac_code/i18n/locales/pt/LC_MESSAGES/webui.po +++ b/src/iac_code/i18n/locales/pt/LC_MESSAGES/webui.po @@ -541,6 +541,14 @@ msgstr "{n}sem" msgid "{n}y" msgstr "{n}a" +#: src/iac_code/web/static/js/app.js +msgid "Operation failed" +msgstr "Falha na operação" + +#: src/iac_code/web/static/js/app.js +msgid "Archive failed" +msgstr "Falha ao arquivar" + #: src/iac_code/web/static/js/app.js msgid "Read-only" msgstr "Somente leitura" @@ -557,14 +565,6 @@ msgstr "Insira o conteúdo" msgid "Please enter a name" msgstr "Insira um nome" -#: src/iac_code/web/static/js/app.js -msgid "Operation failed" -msgstr "Falha na operação" - -#: src/iac_code/web/static/js/app.js -msgid "Archive failed" -msgstr "Falha ao arquivar" - #: src/iac_code/web/static/js/app.js #, python-brace-format msgid "Remove {label}?" 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 a1f9264f..75ed84da 100644 --- a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po @@ -3838,6 +3838,13 @@ msgid "" "with matching parameters and evidence." msgstr "每个用户明确提出的硬约束都必须由一条状态为满足的检查覆盖,且参数和证据一致。" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "" +"The final instance spec must match the architecture plan, budget " +"deviation must be disclosed, and a contract discount must only be " +"reported when it actually reduces the price." +msgstr "最终实例规格必须与架构规划一致,预算偏离必须显式披露,且只有在合同优惠确实带来减免时才可标注优惠价格。" + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "" "Complete the current step by calling this tool to submit the conclusion. " @@ -3929,15 +3936,15 @@ msgid "" "{fields}." msgstr "{message} complete_step.conclusion 必须包含以下字段之一: {fields}。" -#: src/iac_code/pipeline/engine/complete_step_tool.py -msgid "A completion guard is misconfigured." -msgstr "完成守卫配置错误。" - #: src/iac_code/pipeline/engine/complete_step_tool.py #, python-brace-format msgid "{message} Validation issue: {code} ({detail})." msgstr "{message} 校验问题:{code}({detail})。" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "A completion guard is misconfigured." +msgstr "完成守卫配置错误。" + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "A conclusion content hash is required before completing the current step." msgstr "完成当前步骤前需要提供结论内容哈希。" diff --git a/src/iac_code/i18n/locales/zh/LC_MESSAGES/webui.po b/src/iac_code/i18n/locales/zh/LC_MESSAGES/webui.po index b2136c3b..0f2fc8c9 100644 --- a/src/iac_code/i18n/locales/zh/LC_MESSAGES/webui.po +++ b/src/iac_code/i18n/locales/zh/LC_MESSAGES/webui.po @@ -537,6 +537,14 @@ msgstr "{n}周" msgid "{n}y" msgstr "{n}年" +#: src/iac_code/web/static/js/app.js +msgid "Operation failed" +msgstr "操作失败" + +#: src/iac_code/web/static/js/app.js +msgid "Archive failed" +msgstr "归档失败" + #: src/iac_code/web/static/js/app.js msgid "Read-only" msgstr "只读" @@ -553,14 +561,6 @@ msgstr "请输入内容" msgid "Please enter a name" msgstr "请输入名称" -#: src/iac_code/web/static/js/app.js -msgid "Operation failed" -msgstr "操作失败" - -#: src/iac_code/web/static/js/app.js -msgid "Archive failed" -msgstr "归档失败" - #: src/iac_code/web/static/js/app.js #, python-brace-format msgid "Remove {label}?" diff --git a/src/iac_code/pipeline/engine/complete_step_tool.py b/src/iac_code/pipeline/engine/complete_step_tool.py index 7cabdb76..22f18a87 100644 --- a/src/iac_code/pipeline/engine/complete_step_tool.py +++ b/src/iac_code/pipeline/engine/complete_step_tool.py @@ -13,6 +13,7 @@ from iac_code.i18n import _ from iac_code.pipeline.display_names import display_step_name +from iac_code.pipeline.engine.cost_consistency import validate_cost_consistency from iac_code.pipeline.engine.hard_constraints import collect_hard_constraints, validate_hard_constraint_checks from iac_code.pipeline.engine.types import StepResult, StepStatus from iac_code.tools.base import Tool, ToolContext, ToolResult @@ -60,6 +61,10 @@ "Every explicit user hard constraint must be covered by a satisfied check with matching parameters " "and evidence." ), + "cost_plan_consistency_required": ( + "The final instance spec must match the architecture plan, budget deviation must be disclosed, " + "and a contract discount must only be reported when it actually reduces the price." + ), } _COMPLETION_GUARD_MESSAGE_KEY_BY_TEXT = {text: key for key, text in _COMPLETION_GUARD_MESSAGE_TEXT_BY_KEY.items()} @@ -100,6 +105,10 @@ def _completion_guard_message_i18n_markers() -> tuple[str, ...]: "Every explicit user hard constraint must be covered by a satisfied check with matching parameters " "and evidence." ), + _( + "The final instance spec must match the architecture plan, budget deviation must be disclosed, " + "and a contract discount must only be reported when it actually reduces the price." + ), ) @@ -324,6 +333,7 @@ def _validate_completion_guards(self, conclusion: dict) -> str | None: required_tool_result = guard.get("require_tool_result") required_conclusion_sha256 = guard.get("require_conclusion_sha256") required_constraint_coverage = guard.get("require_context_constraint_coverage") + required_cost_consistency = guard.get("require_cost_consistency") required_field = guard.get("required_conclusion_field") required_any_of = guard.get("required_conclusion_any_of") or [] successful_tools = self._completion_guard_state.get("successful_tools", set()) @@ -383,8 +393,54 @@ def _validate_completion_guards(self, conclusion: dict) -> str | None: ) if validation_error is not None: return validation_error + if isinstance(required_cost_consistency, dict): + validation_error = self._validate_cost_consistency( + required_cost_consistency, + conclusion, + self._completion_guard_message(guard, None), + ) + if validation_error is not None: + return validation_error return None + def _validate_cost_consistency( + self, + requirement: dict[str, Any], + conclusion: dict[str, Any], + message: str | None, + ) -> str | None: + context_snapshot = self._completion_guard_state.get("context_snapshot") + if not isinstance(context_snapshot, dict): + context_snapshot = {} + planned_compute = self._resolve_dotted( + context_snapshot, + str(requirement.get("planned_compute_field") or "candidate.planned_compute"), + ) + planned_budget = self._resolve_dotted( + context_snapshot, + str(requirement.get("planned_budget_field") or "candidate.planned_budget"), + ) + issues = validate_cost_consistency( + planned_compute, + planned_budget, + conclusion, + spec_reconciliation_field=str(requirement.get("spec_reconciliation_field") or "spec_reconciliation"), + budget_deviation_field=str(requirement.get("budget_deviation_field") or "budget_deviation"), + ) + if not issues: + return None + base_message = message or _( + "The final instance spec must match the architecture plan, budget deviation must be disclosed, " + "and a contract discount must only be reported when it actually reduces the price." + ) + issue_summaries = [f"{issue.code}[{issue.detail}]" if issue.detail else issue.code for issue in issues] + code = issues[0].code if len(issues) == 1 else "multiple_cost_consistency_issues" + return _("{message} Validation issue: {code} ({detail}).").format( + message=base_message, + code=code, + detail="; ".join(issue_summaries), + ) + def _validate_context_constraint_coverage( self, requirement: dict[str, Any], diff --git a/src/iac_code/pipeline/engine/cost_consistency.py b/src/iac_code/pipeline/engine/cost_consistency.py new file mode 100644 index 00000000..61e09699 --- /dev/null +++ b/src/iac_code/pipeline/engine/cost_consistency.py @@ -0,0 +1,183 @@ +"""Consistency checks between architecture planning baselines and cost estimation results.""" + +from __future__ import annotations + +import re +from dataclasses import asdict, dataclass +from decimal import Decimal, InvalidOperation +from typing import Any + +_AMOUNT_PATTERN = re.compile(r"\d+(?:[.,]\d+)*(?:\.\d+)?") + +BUDGET_WITHIN = "within" +BUDGET_ABOVE = "above" +BUDGET_BELOW = "below" +BUDGET_UNKNOWN = "unknown" + +_SPEC_PARAMETER_NAMES = { + "instance_type": ("InstanceType", "EcsInstanceType"), + "image_id": ("ImageId",), +} + + +@dataclass(frozen=True) +class CostConsistencyIssue: + code: str + detail: str = "" + + def to_dict(self) -> dict[str, str]: + return {key: value for key, value in asdict(self).items() if value} + + +def parse_monthly_amounts(monthly_estimate: Any) -> tuple[Decimal | None, Decimal | None]: + """Extract (list_price, discounted_price) from a rendered monthly estimate string. + + A single amount means only the list price is disclosed; the discounted price is None. + """ + + if not isinstance(monthly_estimate, str): + return None, None + amounts = [amount for amount in (_decimal_or_none(raw) for raw in _AMOUNT_PATTERN.findall(monthly_estimate))] + amounts = [amount for amount in amounts if amount is not None] + if not amounts: + return None, None + if len(amounts) == 1: + return amounts[0], None + return amounts[0], amounts[1] + + +def evaluate_budget_deviation(planned_budget: Any, monthly_estimate: Any) -> tuple[str, Decimal | None]: + """Compare the actual monthly cost against the planned budget range.""" + + actual, _discounted = parse_monthly_amounts(monthly_estimate) + if actual is None: + return BUDGET_UNKNOWN, None + if not isinstance(planned_budget, dict): + return BUDGET_UNKNOWN, actual + minimum = _decimal_or_none(planned_budget.get("monthly_min")) + maximum = _decimal_or_none(planned_budget.get("monthly_max")) + if minimum is None or maximum is None: + return BUDGET_UNKNOWN, actual + if minimum > maximum: + minimum, maximum = maximum, minimum + if actual > maximum: + return BUDGET_ABOVE, actual + if actual < minimum: + return BUDGET_BELOW, actual + return BUDGET_WITHIN, actual + + +def reconcile_instance_spec( + planned_compute: Any, + deployment_parameters: Any, + preview_parameters: Any, +) -> list[CostConsistencyIssue]: + """Verify the planned compute spec is carried unchanged into pricing and preview parameters.""" + + if not isinstance(planned_compute, dict): + return [] + issues: list[CostConsistencyIssue] = [] + deployment = deployment_parameters if isinstance(deployment_parameters, dict) else {} + preview = preview_parameters if isinstance(preview_parameters, dict) else {} + for field, parameter_names in _SPEC_PARAMETER_NAMES.items(): + planned = planned_compute.get(field) + if not isinstance(planned, str) or not planned.strip(): + continue + pricing_value = _first_parameter(deployment, parameter_names) + if pricing_value is None: + issues.append(CostConsistencyIssue("spec_missing_in_deployment_parameters", field)) + elif not _spec_equal(pricing_value, planned): + issues.append(CostConsistencyIssue("spec_deviates_from_plan", f"{field}: {planned} -> {pricing_value}")) + preview_value = _first_parameter(preview, parameter_names) + if preview_value is not None and pricing_value is not None and not _spec_equal(preview_value, pricing_value): + issues.append(CostConsistencyIssue("spec_preview_mismatch", f"{field}: {pricing_value} -> {preview_value}")) + return issues + + +def evaluate_discount_disclosure(monthly_estimate: Any) -> list[CostConsistencyIssue]: + """Reject a contract-discount claim when the discounted price equals the list price.""" + + if not isinstance(monthly_estimate, str) or "合同优惠" not in monthly_estimate: + return [] + list_price, discounted = parse_monthly_amounts(monthly_estimate) + if list_price is None or discounted is None: + return [] + if list_price == discounted: + return [CostConsistencyIssue("discount_without_reduction", f"{list_price}")] + return [] + + +def validate_cost_consistency( + planned_compute: Any, + planned_budget: Any, + conclusion: dict[str, Any], + *, + spec_reconciliation_field: str = "spec_reconciliation", + budget_deviation_field: str = "budget_deviation", +) -> list[CostConsistencyIssue]: + """Validate spec reconciliation, budget deviation disclosure and discount honesty.""" + + monthly_estimate = conclusion.get("monthly_estimate") + deployment_parameters = conclusion.get("deployment_parameters") + preview_validation = conclusion.get("preview_validation") + preview_parameters = preview_validation.get("parameters") if isinstance(preview_validation, dict) else None + + issues = reconcile_instance_spec(planned_compute, deployment_parameters, preview_parameters) + issues.extend(evaluate_discount_disclosure(monthly_estimate)) + + if isinstance(planned_compute, dict) and any( + isinstance(planned_compute.get(field), str) and planned_compute[field].strip() + for field in _SPEC_PARAMETER_NAMES + ): + reconciliation = conclusion.get(spec_reconciliation_field) + if not isinstance(reconciliation, dict): + issues.append(CostConsistencyIssue("missing_spec_reconciliation")) + else: + reported_match = reconciliation.get("matches_plan") + actual_match = not any(issue.code == "spec_deviates_from_plan" for issue in issues) + if reported_match is not actual_match: + issues.append(CostConsistencyIssue("spec_reconciliation_mismatch", str(reported_match))) + if not actual_match and not str(reconciliation.get("deviation_note") or "").strip(): + issues.append(CostConsistencyIssue("missing_spec_deviation_note")) + + status, actual = evaluate_budget_deviation(planned_budget, monthly_estimate) + if status != BUDGET_UNKNOWN: + deviation = conclusion.get(budget_deviation_field) + if not isinstance(deviation, dict): + issues.append(CostConsistencyIssue("missing_budget_deviation")) + else: + if deviation.get("status") != status: + issues.append(CostConsistencyIssue("budget_deviation_status_mismatch", f"{status}")) + reported_actual = _decimal_or_none(deviation.get("actual_monthly")) + if actual is not None and (reported_actual is None or reported_actual != actual): + issues.append(CostConsistencyIssue("budget_deviation_amount_mismatch", f"{actual}")) + if status in {BUDGET_ABOVE, BUDGET_BELOW} and not str(deviation.get("note") or "").strip(): + issues.append(CostConsistencyIssue("missing_budget_deviation_note", status)) + return issues + + +def _first_parameter(parameters: dict[str, Any], names: tuple[str, ...]) -> str | None: + for name in names: + value = parameters.get(name) + if isinstance(value, str) and value.strip(): + return value + return None + + +def _spec_equal(left: str, right: str) -> bool: + return left.strip().casefold() == right.strip().casefold() + + +def _decimal_or_none(value: Any) -> Decimal | None: + if isinstance(value, bool): + return None + if not isinstance(value, (int, float, Decimal, str)): + return None + text = str(value).strip().replace(",", "") + if not text: + return None + try: + number = Decimal(text) + except InvalidOperation: + return None + return number if number.is_finite() else None diff --git a/src/iac_code/pipeline/engine/loader.py b/src/iac_code/pipeline/engine/loader.py index 88f5e298..82539919 100644 --- a/src/iac_code/pipeline/engine/loader.py +++ b/src/iac_code/pipeline/engine/loader.py @@ -43,6 +43,7 @@ "message_key", "require_conclusion_sha256", "require_context_constraint_coverage", + "require_cost_consistency", "require_tool", "require_tool_result", "required_conclusion_any_of", @@ -343,9 +344,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, diff --git a/src/iac_code/pipeline/engine/pipeline_runner.py b/src/iac_code/pipeline/engine/pipeline_runner.py index 59b22a03..a7b4c075 100644 --- a/src/iac_code/pipeline/engine/pipeline_runner.py +++ b/src/iac_code/pipeline/engine/pipeline_runner.py @@ -93,6 +93,8 @@ def _is_a2a_surface(surface: str) -> bool: return surface == "a2a" or surface.startswith("a2a_") + + _SIDECAR_ROOT_DIRS = {"a2a", "image-cache", "pipeline", "tool-results"} _SIDECAR_ROOT_FILES = { ".backup-state.json", diff --git a/src/iac_code/pipeline/selling/pipeline.yaml b/src/iac_code/pipeline/selling/pipeline.yaml index af868cb4..a0b60b42 100644 --- a/src/iac_code/pipeline/selling/pipeline.yaml +++ b/src/iac_code/pipeline/selling/pipeline.yaml @@ -356,6 +356,13 @@ sub_pipelines: checks_field: hard_constraint_checks deployment_parameters_field: deployment_parameters message_key: hard_constraint_verification_required + - always: true + require_cost_consistency: + planned_compute_field: candidate.planned_compute + planned_budget_field: candidate.planned_budget + spec_reconciliation_field: spec_reconciliation + budget_deviation_field: budget_deviation + message_key: cost_plan_consistency_required tools: include: [] exclude: diff --git a/src/iac_code/pipeline/selling/prompts/confirm_and_select.a2a.rich.md b/src/iac_code/pipeline/selling/prompts/confirm_and_select.a2a.rich.md index 04219ce6..ff66d6e5 100644 --- a/src/iac_code/pipeline/selling/prompts/confirm_and_select.a2a.rich.md +++ b/src/iac_code/pipeline/selling/prompts/confirm_and_select.a2a.rich.md @@ -28,6 +28,8 @@ 不要使用 `evaluated_candidates[i].candidate.monthly_estimate`,它只是架构规划阶段的粗略估算。不要重新询价或自行补算价格。若费用明细没有规格字段,省略 `spec`,不要猜测。 +如果 `evaluated_candidates[i].cost.budget_deviation.status` 是 `above` 或 `below`,必须在 `summary` 末尾追加一句偏离标注,说明实际费用相对规划预算区间的方向和 `note` 内容。同理,`cost.spec_reconciliation.matches_plan` 为 `false` 时,在 `summary` 中说明最终规格与规划规格的差异。`status` 为 `within` 或字段缺失时不要添加任何偏离描述。 + `user_prompt`、`summary` 和 Mermaid 节点标签应使用用户当前语言;字段名、枚举值和其他协议字段保持上面的固定形式。 `complete_step.conclusion.user_prompt` 必须是展示给用户的选择提示,例如“请选择要部署的方案:”。 diff --git a/src/iac_code/pipeline/selling/prompts/confirm_and_select.md b/src/iac_code/pipeline/selling/prompts/confirm_and_select.md index 22c0d6af..7f929dbc 100644 --- a/src/iac_code/pipeline/selling/prompts/confirm_and_select.md +++ b/src/iac_code/pipeline/selling/prompts/confirm_and_select.md @@ -50,6 +50,8 @@ 不要使用 `evaluated_candidates[i].candidate.monthly_estimate`,该字段是架构规划阶段的粗略估算。不要重新询价,也不要重新估算价格。 +如果 `evaluated_candidates[i].cost.budget_deviation.status` 是 `above` 或 `below`,必须在 `summary` 末尾追加一句偏离标注,说明实际费用相对规划预算区间的方向和 `note` 内容(例如"实际 ¥443.29/月,超出规划的 ¥200-300/月 区间")。同理,`cost.spec_reconciliation.matches_plan` 为 `false` 时,在 `summary` 中说明最终规格与规划规格的差异。`status` 为 `within` 或字段缺失时不要添加任何偏离描述。 + 如果多个方案都需要展示,必须对每个方案都调用一次“架构图 + 方案详情”的并行展示;不要为了架构图优化额外阻塞方案详情展示。 - 不要用文字输出对比表格或方案信息 — 所有展示数据通过上述工具传递 diff --git a/src/iac_code/pipeline/selling/prompts/cost_estimating.md b/src/iac_code/pipeline/selling/prompts/cost_estimating.md index 559c4bc2..d48c93fa 100644 --- a/src/iac_code/pipeline/selling/prompts/cost_estimating.md +++ b/src/iac_code/pipeline/selling/prompts/cost_estimating.md @@ -6,6 +6,14 @@ ## 当前候选方案 - 名称:`{candidate.name}` +- 规划计算规格: +```json +{candidate.planned_compute} +``` +- 规划月度预算: +```json +{candidate.planned_budget} +``` - 资源生命周期: ```json {candidate.resource_intents} @@ -36,12 +44,15 @@ ## 输出 API 调用完成后调用 `complete_step` 提交费用预估。 -`complete_step.conclusion.monthly_estimate` 必须保留两个价格口径: +`complete_step.conclusion.monthly_estimate` 的价格口径: - `OriginalAmount` 是原价,按统一月度周期换算并汇总为列表价。 - `TradeAmount` 是合同优惠后的最终价,按与原价相同的月度周期换算并汇总。 -- 两个字段都存在时,使用 `¥<原价>/月(列表价,合同优惠后约¥<最终价>/月)` 格式;即使数值相同也保留两个价格口径。 +- `TradeAmount` 低于 `OriginalAmount` 时,使用 `¥<原价>/月(列表价,合同优惠后约¥<最终价>/月)` 格式。 +- 两者相等表示账户没有产生任何减免,只输出 `¥<原价>/月(列表价)`,不得标注合同优惠后价格。 - 任一字段缺失时只展示可用价格,并在 `api_raw_summary` 中说明缺失字段;询价失败时仍填写 `"询价失败"`。 +上方给出了 `candidate.planned_compute` 和 `candidate.planned_budget` 时,还必须按技能的「规划基线一致性核对」输出 `spec_reconciliation` 和 `budget_deviation`:最终规格必须与规划规格一致,确有阻碍才允许偏离并说明原因;实际月度费用超出或低于规划区间时必须显式标注。 + 若 `ros_preview_template` 成功,在 `complete_step.conclusion.preview_validation` 写入 PreviewStack 成功证明:`succeeded: true`、`template_url: "{template.file_path}"`、`parameters: <预览通过的同一参数字典>`;失败或未执行时写入 `succeeded: false`、`error: "<原因>"`。 ## 注意事项 diff --git a/src/iac_code/pipeline/selling/skills/iac-aliyun-architecture/SKILL.md b/src/iac_code/pipeline/selling/skills/iac-aliyun-architecture/SKILL.md index d46d3fb7..e413339e 100644 --- a/src/iac_code/pipeline/selling/skills/iac-aliyun-architecture/SKILL.md +++ b/src/iac_code/pipeline/selling/skills/iac-aliyun-architecture/SKILL.md @@ -83,6 +83,33 @@ conclusion_schema: type: string monthly_estimate: type: string + planned_compute: + type: object + additionalProperties: false + description: 本方案规划的 ECS 计算规格基线;后续成本步骤必须与它 reconcile。方案不包含 ECS 计算资源时省略 + properties: + instance_type: + type: string + description: 规划采用的 ECS 实例类型,如 ecs.t6-c1m1.large;尚未确定具体规格时省略 + image_id: + type: string + description: 规划采用的镜像 ID;尚未确定时省略 + planned_budget: + type: object + required: [monthly_min, monthly_max, currency] + additionalProperties: false + description: monthly_estimate 的结构化区间;后续成本步骤按它判定预算偏离 + properties: + monthly_min: + type: number + description: 规划月度费用下界,与 monthly_estimate 文本一致 + monthly_max: + type: number + description: 规划月度费用上界,与 monthly_estimate 文本一致 + currency: + type: string + enum: [CNY] + description: 预算币种,固定为 CNY pros: type: array items: @@ -134,6 +161,13 @@ conclusion_schema: - 月度费用估算范围 - 优势和局限 +## 规划基线:规格与预算 + +`planned_compute` 和 `planned_budget` 是本步骤向成本估算步骤传递的**可核对基线**,成本步骤会按它们判定规格漂移和预算偏离。 + +- `planned_compute`:方案包含 ECS 计算资源时,填写你规划采用的 `instance_type`(必要时含 `image_id`)。这是后续 `cost_estimating`、`ros_preview_template` 必须沿用的同一规格;不要在这里填一个小规格却预期后续用大规格。方案不含 ECS 计算资源时省略该字段。 +- `planned_budget`:把 `monthly_estimate` 的文本区间同步为结构化的 `monthly_min` / `monthly_max`,两者必须一致(如 `monthly_estimate: "¥50-100/月"` 对应 `monthly_min: 50`、`monthly_max: 100`)。区间要覆盖你对该方案的真实预期,不要为了让后续询价结果落在区间内而人为放宽。 + ## 资源生命周期约束 如果 intent 中存在 `resource_intents`,它是架构设计的硬约束: diff --git a/src/iac_code/pipeline/selling/skills/iac-aliyun-cost/SKILL.md b/src/iac_code/pipeline/selling/skills/iac-aliyun-cost/SKILL.md index dd4ce0a1..30406317 100644 --- a/src/iac_code/pipeline/selling/skills/iac-aliyun-cost/SKILL.md +++ b/src/iac_code/pipeline/selling/skills/iac-aliyun-cost/SKILL.md @@ -17,7 +17,44 @@ conclusion_schema: properties: monthly_estimate: type: string - description: 月度费用估算;询价同时返回 OriginalAmount 与 TradeAmount 时,必须同时包含列表价和合同优惠后价格(如 ¥96.80/月(列表价,合同优惠后约¥13.76/月));询价失败时填 "询价失败" + description: 月度费用估算;询价返回的 TradeAmount 低于 OriginalAmount 时必须同时包含列表价和合同优惠后价格(如 ¥96.80/月(列表价,合同优惠后约¥13.76/月)),两者相等表示账户无合同折扣、只填列表价(如 ¥96.80/月(列表价));询价失败时填 "询价失败" + spec_reconciliation: + type: object + required: [matches_plan] + additionalProperties: false + description: 最终采用的计算规格与 candidate.planned_compute 的核对结果;candidate 提供了规划规格时必填 + properties: + instance_type: + type: string + description: 最终用于询价和预览的 ECS 实例类型 + image_id: + type: string + description: 最终用于询价和预览的镜像 ID + matches_plan: + type: boolean + description: 最终规格是否与 candidate.planned_compute 完全一致 + deviation_note: + type: string + description: matches_plan 为 false 时必填,说明偏离的原因与影响 + budget_deviation: + type: object + required: [status] + additionalProperties: false + description: 实际询价结果与 candidate.planned_budget 的比对结论;candidate 提供了规划预算时必填 + properties: + status: + type: string + enum: [within, above, below, unknown] + description: 实际月度费用相对规划区间的位置 + planned_range: + type: string + description: 规划预算区间的可读表示,如 "¥50-100/月" + actual_monthly: + type: number + description: 从 monthly_estimate 归一化出的实际月度列表价数值 + note: + type: string + description: status 为 above 或 below 时必填,显式说明偏离幅度与原因 currency: type: string enum: [CNY] @@ -198,7 +235,22 @@ conclusion_schema: 5. **按需修复问题** — 仅当询价失败且错误指向模板问题,或你必须修复/改写模板时,修改模板并写回原文件路径 6. **修改后校验并重新询价** — 调用 `ros_validate_template` 校验改动;通过后调用 `ros_estimate_template_cost` 重新询价;失败则修复重试(最多 7 轮) 7. **结构化传递参数** — 在 `complete_step.conclusion.deployment_parameters` 输出当前已选或已用于询价的参数字典;在 `preview_validation` 输出预览成功证明;在 `missing_deployment_parameters` 输出仍未补齐的完整部署参数缺口 -8. **输出结果** — 汇总费用并调用 `complete_step` +8. **核对规划基线** — 按「规划基线一致性核对」比对 `candidate.planned_compute` 与 `candidate.planned_budget`,输出 `spec_reconciliation` 和 `budget_deviation` +9. **输出结果** — 汇总费用并调用 `complete_step` + +## 规划基线一致性核对 + +`candidate.planned_compute` 和 `candidate.planned_budget` 是 architecture_planning 传下来的规划基线。`complete_step` 会用代码逐项核对,删字段或改写结论都无法绕过。 + +**规格一致性**:规划给出 `instance_type`(或 `image_id`)时,它就是本步骤的默认规格。同一个值必须贯穿 `ros_preview_template`、`ros_estimate_template_cost`、`deployment_parameters` 和 `preview_validation.parameters`,不得在询价时换成另一档规格。 + +- 规划规格能满足全部硬约束和库存可用性时,直接沿用,`spec_reconciliation.matches_plan` 填 `true`。 +- 规划规格确实不可用时(违反用户硬约束、库存不足、`AllowedValues` 不包含),才允许改用其它规格,此时 `matches_plan` 填 `false` 并在 `deviation_note` 说明真实原因(如"规划的 ecs.t6-c1m1.large 不满足用户 4 vCPU 硬约束,改用 ecs.c6.xlarge")。不得在没有实际阻碍时"顺手升配"。 +- `spec_reconciliation.instance_type` / `image_id` 必须填最终真实用于询价和预览的值,不是规划值。 + +**预算偏离标注**:规划给出 `planned_budget` 时,把询价得到的月度列表价与 `[monthly_min, monthly_max]` 比对,在 `budget_deviation.status` 填 `within` / `above` / `below`,`actual_monthly` 填归一化后的数值。超出或低于区间时必须在 `note` 里显式说明偏离幅度与原因,不得静默通过。 + +**合同优惠真实性**:`TradeAmount` 等于 `OriginalAmount` 表示该账户没有产生任何减免,此时 `monthly_estimate` 只输出列表价口径,禁止再写"合同优惠后约¥X"——那会让用户误以为存在不存在的折扣。只有 `TradeAmount` 确实低于 `OriginalAmount` 时才输出两个口径。 ## 按需校验模板 @@ -306,4 +358,6 @@ aliyun_api(product="ros", action="GetResourceType", params={"ResourceType": "< - `preview_validation` 填 `ros_preview_template` 的结构化状态:成功时填 `{"succeeded": true, "template_url": "<当前模板文件路径>", "parameters": <预览通过的同一参数字典>}`;失败或未执行时填 `{"succeeded": false, "error": "<原因>"}` - `missing_deployment_parameters` 填完整部署或 PreviewStack 仍缺少的参数及原因;没有缺口时可省略或填 `[]` - `parameter_set_summary` 可简要说明参数来源、可用性筛选、PreviewStack 验证结果以及是否使用软门禁继续询价 +- `spec_reconciliation` 在 candidate 提供 `planned_compute` 时必填,`matches_plan` 为 `false` 时同时填 `deviation_note` +- `budget_deviation` 在 candidate 提供 `planned_budget` 时必填,`status` 为 `above` 或 `below` 时同时填 `note` - 询价失败时 `monthly_estimate` 填 "询价失败",`resources` 为空数组,`error` 说明原因 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_staging.py b/src/iac_code/services/session_backup_staging.py index 6d061c47..e5d933a3 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( diff --git a/tests/a2a/test_executor.py b/tests/a2a/test_executor.py index 4c48e2b7..4e0a31e1 100644 --- a/tests/a2a/test_executor.py +++ b/tests/a2a/test_executor.py @@ -3788,12 +3788,10 @@ def test_accepts_skill_rich_presentation_metadata(self) -> None: executor = self._make_executor() assert ( - executor._resolve_candidate_presentation({"iac_code": {"candidatePresentation": " rich-v1 "}}) - == "rich-v1" + executor._resolve_candidate_presentation({"iac_code": {"candidatePresentation": " rich-v1 "}}) == "rich-v1" ) assert ( - executor._resolve_candidate_presentation({"iac_code": {"candidate_presentation": "RICH-V1"}}) - == "rich-v1" + executor._resolve_candidate_presentation({"iac_code": {"candidate_presentation": "RICH-V1"}}) == "rich-v1" ) def test_rejects_unknown_or_missing_presentation(self) -> None: diff --git a/tests/a2a/test_input_required.py b/tests/a2a/test_input_required.py index b7615e0e..0f310aa7 100644 --- a/tests/a2a/test_input_required.py +++ b/tests/a2a/test_input_required.py @@ -240,9 +240,7 @@ def test_ros_deployment_permission_is_localized_and_preserves_safe_plan_summary( "stackName": "demo-stack", "template": "templates/demo.yml", "totalMonthlyCost": "¥88/月", - "resources": [ - {"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthlyCost": "¥88/月"} - ], + "resources": [{"name": "ECS", "spec": "2 vCPU / 4 GiB", "monthlyCost": "¥88/月"}], }, }, ), @@ -388,9 +386,7 @@ async def record_before_enqueue(envelope): async def test_sub_pipeline_permissions_stay_working_and_resolve_independently(monkeypatch, tmp_path) -> None: registry = PermissionInputRegistry() store = A2ATaskStore() - await store.save( - Task(id="task-1", context_id="ctx-1", status=TaskStatus(state=TaskState.TASK_STATE_WORKING)) - ) + await store.save(Task(id="task-1", context_id="ctx-1", status=TaskStatus(state=TaskState.TASK_STATE_WORKING))) queue = FakeEventQueue() publisher = PipelineA2AEventPublisher( event_queue=queue, @@ -466,9 +462,7 @@ async def test_sub_pipeline_permissions_stay_working_and_resolve_independently(m task = await store.get("task-1") assert task is not None task_metadata = MessageToDict(task.metadata, preserving_proto_field_name=False) - assert [item["inputId"] for item in task_metadata["iac_code"]["pendingPermissions"]] == [ - requests[1]["inputId"] - ] + assert [item["inputId"] for item in task_metadata["iac_code"]["pendingPermissions"]] == [requests[1]["inputId"]] remaining = task_metadata["iac_code"]["pendingPermissions"][0] assert remaining["language"] == "zh" assert remaining["prompt"] == "是否允许本次操作:运行本地 Shell 命令?" diff --git a/tests/pipeline/engine/test_complete_step_tool.py b/tests/pipeline/engine/test_complete_step_tool.py index b47ee908..63f57498 100644 --- a/tests/pipeline/engine/test_complete_step_tool.py +++ b/tests/pipeline/engine/test_complete_step_tool.py @@ -1460,6 +1460,94 @@ async def test_disallowed_file_mutation_matches_relative_and_absolute_paths(self assert result.is_error assert "rerun validation" in result.content + @staticmethod + def _cost_consistency_tool(planned_compute, planned_budget): + return CompleteStepTool( + StepConfig(step_id="cost_estimating", conclusion_field="cost", forward=None), + completion_guards=[ + { + "always": True, + "require_cost_consistency": { + "planned_compute_field": "candidate.planned_compute", + "planned_budget_field": "candidate.planned_budget", + "spec_reconciliation_field": "spec_reconciliation", + "budget_deviation_field": "budget_deviation", + }, + "message_key": "cost_plan_consistency_required", + } + ], + completion_guard_state={ + "context_snapshot": { + "candidate": {"planned_compute": planned_compute, "planned_budget": planned_budget} + } + }, + ) + + def test_cost_consistency_guard_accepts_reconciled_conclusion(self): + tool = self._cost_consistency_tool( + {"instance_type": "ecs.t6-c1m1.large"}, + {"monthly_min": 200, "monthly_max": 300, "currency": "CNY"}, + ) + + error = tool.validate_completion_input( + { + "conclusion": { + "monthly_estimate": "¥281.77/月(列表价)", + "deployment_parameters": {"InstanceType": "ecs.t6-c1m1.large"}, + "spec_reconciliation": {"instance_type": "ecs.t6-c1m1.large", "matches_plan": True}, + "budget_deviation": {"status": "within", "actual_monthly": 281.77}, + } + } + ) + + assert error is None + + def test_cost_consistency_guard_blocks_unreconciled_spec_drift(self): + tool = self._cost_consistency_tool( + {"instance_type": "ecs.t6-c1m1.large"}, + {"monthly_min": 50, "monthly_max": 100, "currency": "CNY"}, + ) + + error = tool.validate_completion_input( + { + "conclusion": { + "monthly_estimate": "¥281.77/月(列表价)", + "deployment_parameters": {"InstanceType": "ecs.g5.large"}, + "spec_reconciliation": {"instance_type": "ecs.g5.large", "matches_plan": True}, + "budget_deviation": {"status": "within", "actual_monthly": 281.77}, + } + } + ) + + assert error is not None + assert "multiple_cost_consistency_issues" in error + assert "spec_deviates_from_plan" in error + assert "budget_deviation_status_mismatch" in error + + def test_cost_consistency_guard_blocks_ineffective_contract_discount(self): + tool = self._cost_consistency_tool(None, None) + + error = tool.validate_completion_input( + {"conclusion": {"monthly_estimate": "¥443.29/月(列表价,合同优惠后约¥443.29/月)"}} + ) + + assert error is not None + assert "discount_without_reduction" in error + + def test_cost_consistency_guard_skips_candidates_without_baseline(self): + tool = self._cost_consistency_tool(None, None) + + error = tool.validate_completion_input( + { + "conclusion": { + "monthly_estimate": "¥443.29/月(列表价)", + "deployment_parameters": {"InstanceType": "ecs.g5.large"}, + } + } + ) + + assert error is None + class TestSchemaValidation: def test_missing_conclusion_validation_error_includes_current_step_schema(self): diff --git a/tests/pipeline/engine/test_cost_consistency.py b/tests/pipeline/engine/test_cost_consistency.py new file mode 100644 index 00000000..0ba9f47c --- /dev/null +++ b/tests/pipeline/engine/test_cost_consistency.py @@ -0,0 +1,215 @@ +from decimal import Decimal + +from iac_code.pipeline.engine.cost_consistency import ( + BUDGET_ABOVE, + BUDGET_BELOW, + BUDGET_UNKNOWN, + BUDGET_WITHIN, + evaluate_budget_deviation, + evaluate_discount_disclosure, + parse_monthly_amounts, + reconcile_instance_spec, + validate_cost_consistency, +) + + +def _codes(issues): + return [issue.code for issue in issues] + + +class TestParseMonthlyAmounts: + def test_parses_list_and_discounted_prices(self): + assert parse_monthly_amounts("¥96.80/月(列表价,合同优惠后约¥13.76/月)") == ( + Decimal("96.80"), + Decimal("13.76"), + ) + + def test_single_amount_has_no_discounted_price(self): + assert parse_monthly_amounts("¥281.77/月(列表价)") == (Decimal("281.77"), None) + + def test_thousands_separator_is_normalized(self): + assert parse_monthly_amounts("¥1,234.50/月(列表价)") == (Decimal("1234.50"), None) + + def test_pricing_failure_yields_no_amount(self): + assert parse_monthly_amounts("询价失败") == (None, None) + assert parse_monthly_amounts(None) == (None, None) + + +class TestEvaluateBudgetDeviation: + def test_within_planned_range(self): + status, actual = evaluate_budget_deviation( + {"monthly_min": 50, "monthly_max": 100, "currency": "CNY"}, + "¥96.80/月(列表价)", + ) + assert status == BUDGET_WITHIN + assert actual == Decimal("96.80") + + def test_session_4a2b12c_exceeds_planned_range(self): + status, actual = evaluate_budget_deviation( + {"monthly_min": 50, "monthly_max": 100, "currency": "CNY"}, + "¥281.77/月(列表价)", + ) + assert status == BUDGET_ABOVE + assert actual == Decimal("281.77") + + def test_session_64ceeb9b_exceeds_planned_range(self): + status, actual = evaluate_budget_deviation( + {"monthly_min": 200, "monthly_max": 300, "currency": "CNY"}, + "¥443.29/月(列表价)", + ) + assert status == BUDGET_ABOVE + assert actual == Decimal("443.29") + + def test_below_planned_range(self): + status, _actual = evaluate_budget_deviation({"monthly_min": 200, "monthly_max": 300}, "¥12/月(列表价)") + assert status == BUDGET_BELOW + + def test_missing_baseline_is_unknown(self): + assert evaluate_budget_deviation(None, "¥100/月")[0] == BUDGET_UNKNOWN + assert evaluate_budget_deviation({"monthly_min": 200}, "¥100/月")[0] == BUDGET_UNKNOWN + + def test_unparsable_estimate_is_unknown(self): + assert evaluate_budget_deviation({"monthly_min": 1, "monthly_max": 2}, "询价失败") == (BUDGET_UNKNOWN, None) + + +class TestReconcileInstanceSpec: + def test_matching_spec_has_no_issue(self): + assert ( + reconcile_instance_spec( + {"instance_type": "ecs.t6-c1m1.large"}, + {"InstanceType": "ecs.t6-c1m1.large"}, + {"InstanceType": "ecs.t6-c1m1.large"}, + ) + == [] + ) + + def test_session_43672b3_spec_drift_is_reported(self): + issues = reconcile_instance_spec( + {"instance_type": "ecs.t6-c1m1.large"}, + {"InstanceType": "ecs.c5.large"}, + {"InstanceType": "ecs.g5.large"}, + ) + assert _codes(issues) == ["spec_deviates_from_plan", "spec_preview_mismatch"] + + def test_missing_pricing_parameter_is_reported(self): + issues = reconcile_instance_spec({"instance_type": "ecs.g7.large"}, {}, {}) + assert _codes(issues) == ["spec_missing_in_deployment_parameters"] + + def test_no_planned_compute_skips_check(self): + assert reconcile_instance_spec(None, {"InstanceType": "ecs.g7.large"}, {}) == [] + assert reconcile_instance_spec({}, {"InstanceType": "ecs.g7.large"}, {}) == [] + + def test_case_and_whitespace_are_normalized(self): + assert reconcile_instance_spec({"instance_type": " ECS.G7.Large "}, {"InstanceType": "ecs.g7.large"}, {}) == [] + + def test_image_id_is_reconciled(self): + issues = reconcile_instance_spec( + {"image_id": "centos_stream_9_x64_20G_alibase_20260414.vhd"}, + {"ImageId": "aliyun_3_x64_20G_alibase_20260101.vhd"}, + {}, + ) + assert _codes(issues) == ["spec_deviates_from_plan"] + + +class TestEvaluateDiscountDisclosure: + def test_real_reduction_is_accepted(self): + assert evaluate_discount_disclosure("¥96.80/月(列表价,合同优惠后约¥13.76/月)") == [] + + def test_session_64ceeb9b_identical_prices_are_rejected(self): + issues = evaluate_discount_disclosure("¥443.29/月(列表价,合同优惠后约¥443.29/月)") + assert _codes(issues) == ["discount_without_reduction"] + + def test_list_price_only_is_accepted(self): + assert evaluate_discount_disclosure("¥443.29/月(列表价)") == [] + + def test_pricing_failure_is_accepted(self): + assert evaluate_discount_disclosure("询价失败") == [] + + +class TestValidateCostConsistency: + def _conclusion(self, **overrides): + conclusion = { + "monthly_estimate": "¥96.80/月(列表价,合同优惠后约¥13.76/月)", + "deployment_parameters": {"InstanceType": "ecs.t6-c1m1.large"}, + "preview_validation": {"succeeded": True, "parameters": {"InstanceType": "ecs.t6-c1m1.large"}}, + "spec_reconciliation": {"instance_type": "ecs.t6-c1m1.large", "matches_plan": True}, + "budget_deviation": {"status": "within", "actual_monthly": 96.80}, + } + conclusion.update(overrides) + return conclusion + + def test_consistent_conclusion_passes(self): + assert ( + validate_cost_consistency( + {"instance_type": "ecs.t6-c1m1.large"}, + {"monthly_min": 50, "monthly_max": 100, "currency": "CNY"}, + self._conclusion(), + ) + == [] + ) + + def test_no_baseline_skips_all_checks(self): + assert validate_cost_consistency(None, None, {"monthly_estimate": "¥281.77/月(列表价)"}) == [] + + def test_missing_spec_reconciliation_is_reported(self): + conclusion = self._conclusion() + del conclusion["spec_reconciliation"] + issues = validate_cost_consistency({"instance_type": "ecs.t6-c1m1.large"}, None, conclusion) + assert _codes(issues) == ["missing_spec_reconciliation"] + + def test_spec_drift_claimed_as_matching_is_reported(self): + conclusion = self._conclusion( + deployment_parameters={"InstanceType": "ecs.c5.large"}, + preview_validation={"succeeded": True, "parameters": {"InstanceType": "ecs.c5.large"}}, + ) + issues = validate_cost_consistency({"instance_type": "ecs.t6-c1m1.large"}, None, conclusion) + assert _codes(issues) == [ + "spec_deviates_from_plan", + "spec_reconciliation_mismatch", + "missing_spec_deviation_note", + ] + + def test_declared_spec_deviation_with_note_is_accepted(self): + conclusion = self._conclusion( + deployment_parameters={"InstanceType": "ecs.c6.xlarge"}, + preview_validation={"succeeded": True, "parameters": {"InstanceType": "ecs.c6.xlarge"}}, + spec_reconciliation={ + "instance_type": "ecs.c6.xlarge", + "matches_plan": False, + "deviation_note": "规划规格不满足用户 4 vCPU 硬约束", + }, + ) + issues = validate_cost_consistency({"instance_type": "ecs.t6-c1m1.large"}, None, conclusion) + assert _codes(issues) == ["spec_deviates_from_plan"] + + def test_missing_budget_deviation_is_reported(self): + conclusion = self._conclusion() + del conclusion["budget_deviation"] + issues = validate_cost_consistency(None, {"monthly_min": 50, "monthly_max": 100}, conclusion) + assert _codes(issues) == ["missing_budget_deviation"] + + def test_unreported_budget_overrun_is_reported(self): + conclusion = self._conclusion(monthly_estimate="¥443.29/月(列表价)") + issues = validate_cost_consistency(None, {"monthly_min": 200, "monthly_max": 300}, conclusion) + assert _codes(issues) == [ + "budget_deviation_status_mismatch", + "budget_deviation_amount_mismatch", + "missing_budget_deviation_note", + ] + + def test_declared_budget_overrun_with_note_is_accepted(self): + conclusion = self._conclusion( + monthly_estimate="¥443.29/月(列表价)", + budget_deviation={ + "status": "above", + "planned_range": "¥200-300/月", + "actual_monthly": 443.29, + "note": "通用方案实际费用超出规划区间约 48%", + }, + ) + assert validate_cost_consistency(None, {"monthly_min": 200, "monthly_max": 300}, conclusion) == [] + + def test_ineffective_discount_is_reported(self): + conclusion = self._conclusion(monthly_estimate="¥443.29/月(列表价,合同优惠后约¥443.29/月)") + issues = validate_cost_consistency(None, None, conclusion) + assert _codes(issues) == ["discount_without_reduction"] diff --git a/tests/pipeline/engine/test_step_executor.py b/tests/pipeline/engine/test_step_executor.py index 2d596efe..c85dc073 100644 --- a/tests/pipeline/engine/test_step_executor.py +++ b/tests/pipeline/engine/test_step_executor.py @@ -2776,11 +2776,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", @@ -2863,11 +2867,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/skills/test_iac_aliyun_architecture_skill.py b/tests/pipeline/selling/skills/test_iac_aliyun_architecture_skill.py index d133f258..9e54cc3b 100644 --- a/tests/pipeline/selling/skills/test_iac_aliyun_architecture_skill.py +++ b/tests/pipeline/selling/skills/test_iac_aliyun_architecture_skill.py @@ -48,6 +48,31 @@ def test_architecture_hard_constraint_schema_describes_every_field(): assert all(value.get("description") for value in properties.values()) +def test_architecture_candidate_schema_declares_planning_baselines(): + body = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8") + end = body.index("---", 3) + schema = yaml.safe_load(body[3:end])["conclusion_schema"] + properties = schema["properties"]["candidates"]["items"]["properties"] + + planned_compute = properties["planned_compute"] + assert set(planned_compute["properties"]) == {"instance_type", "image_id"} + assert planned_compute["additionalProperties"] is False + + planned_budget = properties["planned_budget"] + assert planned_budget["required"] == ["monthly_min", "monthly_max", "currency"] + assert planned_budget["properties"]["currency"]["enum"] == ["CNY"] + assert all(value.get("description") for value in planned_budget["properties"].values()) + + +def test_architecture_body_requires_truthful_planning_baselines(): + body = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8") + + assert "## 规划基线:规格与预算" in body + assert "planned_compute" in body + assert "planned_budget" in body + assert "monthly_estimate" in body + + def test_architecture_prompt_guides_optional_memory_lookup_for_planning_context(): body = PROMPT_FILE.read_text(encoding="utf-8") diff --git a/tests/pipeline/selling/skills/test_iac_aliyun_cost_skill.py b/tests/pipeline/selling/skills/test_iac_aliyun_cost_skill.py index 2d46be8d..c3073bdf 100644 --- a/tests/pipeline/selling/skills/test_iac_aliyun_cost_skill.py +++ b/tests/pipeline/selling/skills/test_iac_aliyun_cost_skill.py @@ -172,6 +172,30 @@ def test_monthly_estimate_schema_describes_list_and_discounted_prices(self): assert "OriginalAmount" in description assert "TradeAmount" in description + def test_monthly_estimate_schema_forbids_discount_caliber_without_reduction(self): + content = SKILL_MD.read_text(encoding="utf-8") + fm = _parse_frontmatter(content) + description = fm["conclusion_schema"]["properties"]["monthly_estimate"]["description"] + + assert "两者相等" in description + assert "只填列表价" in description + + def test_conclusion_schema_carries_plan_reconciliation_fields(self): + content = SKILL_MD.read_text(encoding="utf-8") + fm = _parse_frontmatter(content) + properties = fm["conclusion_schema"]["properties"] + + spec = properties["spec_reconciliation"] + assert spec["required"] == ["matches_plan"] + assert set(spec["properties"]) == {"instance_type", "image_id", "matches_plan", "deviation_note"} + assert "planned_compute" in spec["description"] + + budget = properties["budget_deviation"] + assert budget["required"] == ["status"] + assert budget["properties"]["status"]["enum"] == ["within", "above", "below", "unknown"] + assert set(budget["properties"]) == {"status", "planned_range", "actual_monthly", "note"} + assert "planned_budget" in budget["description"] + def test_conclusion_schema_requires_full_preview_validation_when_succeeded(self): content = SKILL_MD.read_text(encoding="utf-8") fm = _parse_frontmatter(content) @@ -539,6 +563,21 @@ def test_prompt_requires_list_and_discounted_monthly_prices(self): assert "合同优惠后" in body assert "monthly_estimate" in body + def test_prompt_forbids_reporting_a_discount_that_does_not_reduce_the_price(self): + body = COST_PROMPT_MD.read_text(encoding="utf-8") + + assert "即使数值相同也保留两个价格口径" not in body + assert "两者相等表示账户没有产生任何减免" in body + assert "不得标注合同优惠后价格" in body + + def test_prompt_passes_planning_baselines_and_requires_reconciliation(self): + body = COST_PROMPT_MD.read_text(encoding="utf-8") + + assert "{candidate.planned_compute}" in body + assert "{candidate.planned_budget}" in body + assert "spec_reconciliation" in body + assert "budget_deviation" in body + def test_prompt_receives_only_required_candidate_fields_without_repeating_skill_rules(self): body = COST_PROMPT_MD.read_text(encoding="utf-8") diff --git a/tests/pipeline/selling/test_pipeline_reviewing.py b/tests/pipeline/selling/test_pipeline_reviewing.py index 531ced8a..2f0a2016 100644 --- a/tests/pipeline/selling/test_pipeline_reviewing.py +++ b/tests/pipeline/selling/test_pipeline_reviewing.py @@ -44,7 +44,17 @@ def test_review_enabled_loads_infraguard_repair_step_before_cost() -> None: "deployment_parameters_field": "deployment_parameters", }, "message_key": "hard_constraint_verification_required", - } + }, + { + "always": True, + "require_cost_consistency": { + "planned_compute_field": "candidate.planned_compute", + "planned_budget_field": "candidate.planned_budget", + "spec_reconciliation_field": "spec_reconciliation", + "budget_deviation_field": "budget_deviation", + }, + "message_key": "cost_plan_consistency_required", + }, ] assert review_step.tools is not None 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/providers/test_manager.py b/tests/providers/test_manager.py index 8ff0fc1a..54057e5f 100644 --- a/tests/providers/test_manager.py +++ b/tests/providers/test_manager.py @@ -1676,9 +1676,7 @@ async def test_complete_records_chat_span_event_and_total_metric(self): async def test_complete_attributes_bailian_openai_endpoint_to_dashscope_on_all_signals(self): mock_provider = AsyncMock() - mock_provider._base_url = ( - "https://llm-testworkspace000000.cn-beijing.maas.aliyuncs.com/compatible-mode/v1" - ) + mock_provider._base_url = "https://llm-testworkspace000000.cn-beijing.maas.aliyuncs.com/compatible-mode/v1" mock_provider.complete = AsyncMock( return_value=NonStreamingResponse( message_id="complete-response", diff --git a/tests/skill_bridge/test_iac_code_bridge.py b/tests/skill_bridge/test_iac_code_bridge.py index b7e9280f..1eafe79e 100644 --- a/tests/skill_bridge/test_iac_code_bridge.py +++ b/tests/skill_bridge/test_iac_code_bridge.py @@ -542,12 +542,10 @@ def finish_cleanup(_args): assert len(captured_payloads) == 2 assert all( - payload["params"]["message"]["metadata"]["iac_code"]["cleanupOnly"] is True - for payload in captured_payloads + payload["params"]["message"]["metadata"]["iac_code"]["cleanupOnly"] is True for payload in captured_payloads ) assert all( - payload["params"]["message"]["metadata"]["iac_code"]["channel"] == "skill/host" - for payload in captured_payloads + payload["params"]["message"]["metadata"]["iac_code"]["channel"] == "skill/host" for payload in captured_payloads ) assert captured_payloads[0]["params"]["message"]["contextId"] == "ctx-pipeline-1" assert result["state"] == "completed" @@ -1138,9 +1136,7 @@ def test_candidate_presentation_survives_bounded_bridge_projection() -> None: "summary": "单 ECS 低成本方案。", "architectureDiagram": "flowchart LR\nU[用户] --> E[ECS]", "totalMonthlyCost": "¥88/月", - "costItems": [ - {"name": "ECS", "spec": "2核4G", "monthlyCost": "¥88/月"} - ], + "costItems": [{"name": "ECS", "spec": "2核4G", "monthlyCost": "¥88/月"}], } ], "required": True, diff --git a/tests/skill_bridge/test_runtime_release.py b/tests/skill_bridge/test_runtime_release.py index 140ebb59..d6de0578 100644 --- a/tests/skill_bridge/test_runtime_release.py +++ b/tests/skill_bridge/test_runtime_release.py @@ -104,9 +104,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/web/test_frontend_static.py b/tests/web/test_frontend_static.py index c3c6f30f..fe9e6f69 100644 --- a/tests/web/test_frontend_static.py +++ b/tests/web/test_frontend_static.py @@ -2547,7 +2547,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)连同思考、工具一起折进「已处理」,不平铺成答案。