From fc428ef3be3adfc84bbb3995595ae98d295f4d06 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:15:52 +0800 Subject: [PATCH] fix(console): remove duplicate option layer and classify stop failures Co-Authored-By: Codex --- python/scripts/runtime_settings.py | 33 +++++++++++++-- python/tests/test_runtime_stop.py | 40 ++++++++++++++++++- tests/console_runtime_state_validation.mjs | 6 +++ .../frontend/src/AccountsPage.tsx | 1 - web/strategy-switch-console/v2_asset_map.js | 4 +- 5 files changed, 77 insertions(+), 7 deletions(-) diff --git a/python/scripts/runtime_settings.py b/python/scripts/runtime_settings.py index 00dc7bb..f1d3f6a 100644 --- a/python/scripts/runtime_settings.py +++ b/python/scripts/runtime_settings.py @@ -1361,12 +1361,20 @@ def read_stop_variables(github: dict[str, Any]) -> dict[str, str]: else: endpoint += "/actions" endpoint += "/variables?per_page=100" + reason = "stop_source_unavailable" try: result = subprocess.run( ["gh", "api", "--method", "GET", "--paginate", "--slurp", endpoint], capture_output=True, text=True, timeout=60, check=False, ) if result.returncode != 0: + # Classify the response without ever printing private GH output. + if "unknown flag: --slurp" in result.stderr: + reason = "stop_source_cli_unsupported" + elif "HTTP 401" in result.stderr or "HTTP 403" in result.stderr: + reason = "stop_source_permission_denied" + elif "HTTP 404" in result.stderr: + reason = "stop_source_not_accessible" raise ValueError pages = json.loads(result.stdout) if not isinstance(pages, list) or not pages: @@ -1384,10 +1392,13 @@ def read_stop_variables(github: dict[str, Any]) -> dict[str, str]: raise ValueError values[name] = value if len(values) != total: + reason = "stop_source_incomplete" raise ValueError return values + except subprocess.TimeoutExpired: + raise ValueError("stop_source_timeout") from None except (OSError, subprocess.SubprocessError, ValueError, TypeError, KeyError, IndexError): - raise ValueError("stop_source_unavailable") from None + raise ValueError(reason) from None def read_disabled_profile_apply_variables(github: dict[str, Any]) -> dict[str, dict[str, str]]: @@ -1733,8 +1744,10 @@ def command_stop(args: argparse.Namespace) -> int: if args.yes and args.confirm != "STOP_ONLY": print("stop requires --confirm STOP_ONLY for writes", file=sys.stderr) return 2 + stage = "event" try: request = load_stop_request() + stage = "configuration" apply_hk_stop = getattr(args, "apply_hk_stop", False) if apply_hk_stop: if not args.yes: @@ -1742,6 +1755,7 @@ def command_stop(args: argparse.Namespace) -> int: require_hk_stop_target(request) require_production_writer_ref() saved = execute_stop(request, apply=True) + stage = "platform_dispatch" dispatch_hk_stop(request) result = { **saved, @@ -1751,9 +1765,22 @@ def command_stop(args: argparse.Namespace) -> int: } else: result = execute_stop(request, apply=args.yes) - except (OSError, ValueError, TypeError, KeyError): + except (OSError, ValueError, TypeError, KeyError) as error: # Underlying errors and private target/config values stay out of logs. - print("stop_not_verified; do not retry or infer platform state", file=sys.stderr) + safe_reasons = { + "stop_event_unverified", "stop_source_unavailable", "stop_source_cli_unsupported", + "stop_source_permission_denied", "stop_source_not_accessible", "stop_source_incomplete", + "stop_source_timeout", "stop_source_changed", "stop_write_outcome_unverified", + "stop_readback_unverified", "stale_writer_ref_rejected", "stop_platform_target_unsupported", + "stop_saved_configuration_required", "stop_platform_dispatch_unverified", + "stop_platform_apply_requires_saved_stop", + } + reason = str(error) + if reason == "stop requires an unambiguous current target with matching identity": + reason = "stop_identity_mismatch" + elif reason not in safe_reasons: + reason = "stop_event_unverified" if stage == "event" else "stop_configuration_unverified" + print(f"stop_not_verified; stage={stage}; reason={reason}; do not retry or infer platform state", file=sys.stderr) return 2 print(json.dumps(result, sort_keys=True)) return 0 diff --git a/python/tests/test_runtime_stop.py b/python/tests/test_runtime_stop.py index a13ef4d..bdea880 100644 --- a/python/tests/test_runtime_stop.py +++ b/python/tests/test_runtime_stop.py @@ -259,9 +259,47 @@ def test_variable_read_checks_complete_pagination_without_output(self): for output in bad: with self.subTest(output=output), patch.object(runtime_settings.subprocess, "run", return_value= subprocess.CompletedProcess([], 0, output, "synthetic-sensitive-error")), \ - self.assertRaisesRegex(ValueError, "^stop_source_unavailable$"): + self.assertRaisesRegex(ValueError, "^stop_source_(unavailable|incomplete)$"): runtime_settings.read_stop_variables(self.request["github"]) + def test_variable_reader_reports_only_fixed_failure_categories(self): + cases = [ + ("unknown flag: --slurp", "stop_source_cli_unsupported"), + ("private endpoint (HTTP 403)", "stop_source_permission_denied"), + ("private endpoint (HTTP 401)", "stop_source_permission_denied"), + ("private endpoint (HTTP 404)", "stop_source_not_accessible"), + ("private transport details", "stop_source_unavailable"), + ] + for stderr, reason in cases: + with self.subTest(reason=reason), patch.object(runtime_settings.subprocess, "run", return_value= + subprocess.CompletedProcess([], 1, "private body", stderr)), \ + self.assertRaisesRegex(ValueError, f"^{reason}$"): + runtime_settings.read_stop_variables(self.request["github"]) + with patch.object(runtime_settings.subprocess, "run", side_effect=subprocess.TimeoutExpired("private", 60)), \ + self.assertRaisesRegex(ValueError, "^stop_source_timeout$"): + runtime_settings.read_stop_variables(self.request["github"]) + + def test_cli_failure_is_diagnostic_without_exposing_details_or_retrying(self): + for error, reason in [ + (ValueError("stop_source_permission_denied"), "stop_source_permission_denied"), + (ValueError("stop_write_outcome_unverified"), "stop_write_outcome_unverified"), + (ValueError("private target and token"), "stop_configuration_unverified"), + (ValueError("stop_source_timeout private token"), "stop_configuration_unverified"), + ]: + with self.subTest(reason=reason), patch.object(runtime_settings, "load_stop_request", return_value=self.request), \ + patch.object(runtime_settings, "execute_stop", side_effect=error) as execute, \ + contextlib.redirect_stderr(io.StringIO()) as stderr: + self.assertEqual(runtime_settings.main(["stop", "--yes", "--confirm", "STOP_ONLY"]), 2) + execute.assert_called_once() + self.assertEqual(stderr.getvalue(), + f"stop_not_verified; stage=configuration; reason={reason}; do not retry or infer platform state\n") + with patch.object(runtime_settings, "load_stop_request", side_effect=KeyError("private event")), \ + patch.object(runtime_settings, "execute_stop") as execute, contextlib.redirect_stderr(io.StringIO()) as stderr: + self.assertEqual(runtime_settings.main(["stop"]), 2) + execute.assert_not_called() + self.assertIn("stage=event; reason=stop_event_unverified", stderr.getvalue()) + self.assertNotIn("private", stderr.getvalue()) + def test_variable_reader_uses_actions_for_repository_and_encodes_environment(self): repository = "QuantStrategyLab/BinancePlatform" for scope, expected in [ diff --git a/tests/console_runtime_state_validation.mjs b/tests/console_runtime_state_validation.mjs index 03dac40..0a855f6 100644 --- a/tests/console_runtime_state_validation.mjs +++ b/tests/console_runtime_state_validation.mjs @@ -76,6 +76,12 @@ function sources(overrides = {}) { }; } +test("pending option layer has one editable label across draft states", () => { + assert.equal((accountsSource.match(/t\("待应用期权层"\)/g) || []).length, 1); + assert.match(accountsSource, /