From 769b71bfac9f7a6ef65255cc823ae3aba695e693 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:32:36 +0800 Subject: [PATCH] Drop legacy runtime routes --- .github/workflows/invoke-cloud-run.yml | 18 +++++----- .github/workflows/sync-cloud-run-env.yml | 16 ++++++++- main.py | 22 +++--------- notifications/telegram.py | 6 ++-- tests/test_invoke_cloud_run_workflow.sh | 6 ++-- tests/test_notifications.py | 10 +++--- tests/test_request_handling.py | 45 ++++++------------------ 7 files changed, 50 insertions(+), 73 deletions(-) diff --git a/.github/workflows/invoke-cloud-run.yml b/.github/workflows/invoke-cloud-run.yml index b3d686d..0e09e8c 100644 --- a/.github/workflows/invoke-cloud-run.yml +++ b/.github/workflows/invoke-cloud-run.yml @@ -15,10 +15,10 @@ on: path: description: "HTTP path to call" required: false - default: "/precheck" + default: "/dry-run" type: string allow_live_execution: - description: "Allow calling / live execution entrypoint" + description: "Allow calling /run live execution entrypoint" required: false default: false type: boolean @@ -82,12 +82,12 @@ jobs: raw_path="${{ inputs.path }}" if [ -z "${raw_path}" ]; then - raw_path="/" + raw_path="/dry-run" fi if [[ "${raw_path}" != /* ]]; then raw_path="/${raw_path}" fi - if { [ "${raw_path}" = "/" ] || [ "${raw_path}" = "/run" ]; } && [ "${{ inputs.allow_live_execution }}" != "true" ]; then + if [ "${raw_path}" = "/run" ] && [ "${{ inputs.allow_live_execution }}" != "true" ]; then echo "Calling ${raw_path} can trigger the live execution entrypoint. Re-run with allow_live_execution=true if this is intentional." >&2 exit 1 fi @@ -144,21 +144,21 @@ jobs: if [ "${service_ingress}" = "internal" ]; then scheduler_location="${CLOUD_SCHEDULER_LOCATION:-${CLOUD_RUN_REGION}}" case "${raw_path}" in - /|/run) + /run) scheduler_job="${CLOUD_RUN_SERVICE}-scheduler" - scheduler_expected_path="/" + scheduler_expected_path="/run" ;; /probe) scheduler_job="${CLOUD_RUN_SERVICE}-probe-scheduler" scheduler_expected_path="/probe" ;; - /precheck|/dry-run) + /dry-run) scheduler_job="${CLOUD_RUN_SERVICE}-precheck-scheduler" - scheduler_expected_path="/precheck" + scheduler_expected_path="/dry-run" ;; *) echo "Cloud Run service ${CLOUD_RUN_SERVICE} has internal ingress, so GitHub-hosted runners cannot curl ${raw_path} directly." >&2 - echo "Use one of the scheduler-backed paths: /, /run, /probe, /precheck, /dry-run." >&2 + echo "Use one of the scheduler-backed paths: /run, /probe, /dry-run." >&2 exit 1 ;; esac diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index 7986b5e..99bad64 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -991,17 +991,29 @@ jobs: probe_time="${scheduler_market_config[2]}" precheck_time="${scheduler_market_config[3]}" + service_url="$(gcloud run services describe "${CLOUD_RUN_SERVICE}" \ + --project="${GCP_PROJECT_ID}" \ + --region="${CLOUD_RUN_REGION}" \ + --format='value(status.url)' 2>/dev/null || true)" + if [ -z "${service_url}" ]; then + echo "Unable to resolve Cloud Run service URL for ${CLOUD_RUN_SERVICE}; cannot sync scheduler URI." >&2 + exit 1 + fi + for suffix in scheduler probe-scheduler precheck-scheduler; do job_name="${CLOUD_RUN_SERVICE}-${suffix}" case "${suffix}" in scheduler) schedule_time="${main_time}" + scheduler_path="/run" ;; probe-scheduler) schedule_time="${probe_time}" + scheduler_path="/probe" ;; precheck-scheduler) schedule_time="${precheck_time}" + scheduler_path="/dry-run" ;; esac @@ -1027,10 +1039,12 @@ jobs: PY )" - echo "Updating Cloud Scheduler job ${job_name} schedule to ${desired_schedule} and timezone to ${market_timezone}." + scheduler_uri="${service_url}${scheduler_path}" + echo "Updating Cloud Scheduler job ${job_name} schedule to ${desired_schedule}, timezone to ${market_timezone}, and URI to ${scheduler_uri}." gcloud scheduler jobs update http "${job_name}" \ --project="${GCP_PROJECT_ID}" \ --location="${scheduler_location}" \ + --uri="${scheduler_uri}" \ --schedule="${desired_schedule}" \ --time-zone="${market_timezone}" \ --quiet diff --git a/main.py b/main.py index 47ed42c..ae89049 100644 --- a/main.py +++ b/main.py @@ -518,8 +518,8 @@ def run_strategy(*, force_run: bool = False, validation_only: bool = False, vali strategy_plugin_signals=strategy_plugin_signals, strategy_plugin_error=strategy_plugin_error, notification_title_key=( - "precheck_title" - if validation_only and validation_label == "precheck" + "dry_run_title" + if validation_only and validation_label == "dry_run" else "" ), ), @@ -672,7 +672,6 @@ def run_probe(*, response_body: str = "Probe OK"): print(f"failed to persist execution report: {persist_exc}", flush=True) -@app.route("/", methods=["POST", "GET"]) @app.route("/run", methods=["POST", "GET"]) def handle_trigger(): """Entrypoint for Cloud Run / scheduler: run strategy and return 200.""" @@ -695,27 +694,14 @@ def handle_backfill(): ) -@app.route("/precheck", methods=["POST", "GET"]) -def handle_precheck(): - """Pre-market / post-open verification entrypoint for dry-run only execution.""" - return _route_with_runtime_error_fallback( - run_strategy, - force_run=True, - validation_only=True, - validation_label="precheck", - success_body="Precheck OK", - route_label="POST /precheck", - ) - - @app.route("/dry-run", methods=["POST", "GET"]) def handle_dry_run(): - """Strategy dry-run entrypoint; alias of precheck with clearer operator wording.""" + """Strategy dry-run entrypoint.""" return _route_with_runtime_error_fallback( run_strategy, force_run=True, validation_only=True, - validation_label="precheck", + validation_label="dry_run", success_body="Dry Run OK", route_label="POST /dry-run", ) diff --git a/notifications/telegram.py b/notifications/telegram.py index 877cb62..83f916b 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -39,7 +39,8 @@ "income_locked": "🏦 收入层锁定占比: {ratio}", "signal": "🎯 触发信号: {msg}", "heartbeat_title": "💓 【心跳检测】", - "precheck_title": "🧪 【策略预检】", + "precheck_title": "🧪 【策略演练】", + "dry_run_title": "🧪 【策略演练】", "health_probe_title": "🔎 【连接探针】", "health_probe_error_prefix": "健康探针异常:\n", "equity": "💰 净值: ${value}", @@ -197,7 +198,8 @@ "income_locked": "🏦 Income Locked: {ratio}", "signal": "🎯 Signal: {msg}", "heartbeat_title": "💓 【Heartbeat】", - "precheck_title": "🧪 【Strategy Precheck】", + "precheck_title": "🧪 【Strategy Dry Run】", + "dry_run_title": "🧪 【Strategy Dry Run】", "health_probe_title": "🔎 【Health Probe】", "health_probe_error_prefix": "Health probe error:\n", "equity": "💰 Equity: ${value}", diff --git a/tests/test_invoke_cloud_run_workflow.sh b/tests/test_invoke_cloud_run_workflow.sh index 5d6ed8a..b740400 100644 --- a/tests/test_invoke_cloud_run_workflow.sh +++ b/tests/test_invoke_cloud_run_workflow.sh @@ -7,9 +7,9 @@ workflow_file="$repo_dir/.github/workflows/invoke-cloud-run.yml" grep -Fq "name: Invoke Cloud Run" "$workflow_file" grep -Fq "workflow_dispatch:" "$workflow_file" grep -Fq "environment: \${{ inputs.environment }}" "$workflow_file" -grep -Fq 'default: "/precheck"' "$workflow_file" +grep -Fq 'default: "/dry-run"' "$workflow_file" grep -Fq "allow_live_execution:" "$workflow_file" -grep -Fq "Calling / can trigger the live execution entrypoint." "$workflow_file" +grep -Fq "Calling \${raw_path} can trigger the live execution entrypoint." "$workflow_file" grep -Fq "id-token: write" "$workflow_file" grep -Fq "google-github-actions/auth@v3" "$workflow_file" grep -Fq "google-github-actions/setup-gcloud@v3" "$workflow_file" @@ -19,7 +19,7 @@ grep -Fq "CLOUD_SCHEDULER_LOCATION: \${{ vars.CLOUD_SCHEDULER_LOCATION }}" "$wor grep -Fq "longbridge-hk|longbridge-paper|longbridge-sg" "$workflow_file" grep -Fq "gcloud run services describe \"\${CLOUD_RUN_SERVICE}\"" "$workflow_file" grep -Fq "Cloud Run service \${CLOUD_RUN_SERVICE} has internal ingress" "$workflow_file" -grep -Fq "Use one of the scheduler-backed paths: /, /probe, /precheck." "$workflow_file" +grep -Fq "Use one of the scheduler-backed paths: /run, /probe, /dry-run." "$workflow_file" grep -Fq "scheduler_job=\"\${CLOUD_RUN_SERVICE}-precheck-scheduler\"" "$workflow_file" grep -Fq "Invoke internal service through Cloud Scheduler" "$workflow_file" grep -Fq "gcloud scheduler jobs run \"\${scheduler_job}\"" "$workflow_file" diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 9494e38..a4f0da7 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -177,7 +177,7 @@ def test_heartbeat_signal_snapshot_localizes_price_source(self): self.assertIn("📊 市场状态: 🚀 风险开启(SOXX+SOXL)", rendered.compact_text) self.assertNotIn("longbridge_candlesticks", rendered.compact_text) - def test_precheck_heartbeat_uses_precheck_title(self): + def test_dry_run_heartbeat_uses_dry_run_title(self): rendered = render_heartbeat_notification( execution={ "signal_display": "🚀 入场信号 | 原因:QQQ 高于 MA200", @@ -188,7 +188,7 @@ def test_precheck_heartbeat_uses_precheck_title(self): separator="━━━━━━━━━━━━━━━━━━", strategy_display_name="TQQQ 增长收益", dry_run_only=True, - title_key="precheck_title", + title_key="dry_run_title", ) en_rendered = render_heartbeat_notification( execution={ @@ -200,12 +200,12 @@ def test_precheck_heartbeat_uses_precheck_title(self): separator="━━━━━━━━━━━━━━━━━━", strategy_display_name="TQQQ Growth Income", dry_run_only=True, - title_key="precheck_title", + title_key="dry_run_title", ) - self.assertIn("🧪 【策略预检】", rendered.compact_text) + self.assertIn("🧪 【策略演练】", rendered.compact_text) self.assertNotIn("💓 【心跳检测】", rendered.compact_text) - self.assertIn("🧪 【Strategy Precheck】", en_rendered.compact_text) + self.assertIn("🧪 【Strategy Dry Run】", en_rendered.compact_text) self.assertNotIn("💓 【Heartbeat】", en_rendered.compact_text) def test_heartbeat_renders_tqqq_volatility_delever_risk_control(self): diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index 0608653..ad6003f 100644 --- a/tests/test_request_handling.py +++ b/tests/test_request_handling.py @@ -203,16 +203,11 @@ class RequestHandlingTests(unittest.TestCase): def test_cloud_run_route_contracts_are_registered(self): module = load_module() - self.assertIs(module.app._routes[("/", ("POST", "GET"))], module.handle_trigger) self.assertIs(module.app._routes[("/run", ("POST", "GET"))], module.handle_trigger) self.assertIs( module.app._routes[("/backfill", ("POST", "GET"))], module.handle_backfill, ) - self.assertIs( - module.app._routes[("/precheck", ("POST", "GET"))], - module.handle_precheck, - ) self.assertIs( module.app._routes[("/dry-run", ("POST", "GET"))], module.handle_dry_run, @@ -249,7 +244,7 @@ def fake_run_strategy(): }, clear=False, ): - with module.app.test_request_context("/", method="POST"): + with module.app.test_request_context("/run", method="POST"): body, status = module.handle_trigger() self.assertEqual(status, 200) @@ -260,7 +255,7 @@ def test_handle_trigger_returns_500_when_strategy_reports_failure(self): module = load_module() module.run_strategy = lambda: False - with module.app.test_request_context("/", method="POST"): + with module.app.test_request_context("/run", method="POST"): body, status = module.handle_trigger() self.assertEqual(status, 500) @@ -282,7 +277,7 @@ def fake_post(_url, *, json, timeout): module.requests.post = fake_post module.run_strategy = lambda: (_ for _ in ()).throw(RuntimeError("boom")) - with module.app.test_request_context("/", method="POST"): + with module.app.test_request_context("/run", method="POST"): body, status = module.handle_trigger() self.assertEqual(status, 500) @@ -308,7 +303,7 @@ def fake_post(_url, *, json, timeout): module.requests.post = fake_post module.run_strategy = lambda: (_ for _ in ()).throw(RuntimeError("boom")) - with module.app.test_request_context("/", method="POST"): + with module.app.test_request_context("/run", method="POST"): body, status = module.handle_trigger() self.assertEqual(status, 500) @@ -327,7 +322,7 @@ def fake_run_strategy(): module.run_strategy = fake_run_strategy - with module.app.test_request_context("/", method="GET"): + with module.app.test_request_context("/run", method="GET"): body, status = module.handle_trigger() self.assertEqual(status, 200) @@ -351,27 +346,7 @@ def fake_run_strategy(*, force_run=False, validation_only=False, validation_labe self.assertEqual(body, "OK") self.assertTrue(observed["force_run"]) self.assertTrue(observed["validation_only"]) - def test_handle_precheck_forces_strategy_run(self): - module = load_module() - observed = {"force_run": None, "validation_only": None} - - def fake_run_strategy(*, force_run=False, validation_only=False, validation_label="backfill"): - observed["force_run"] = force_run - observed["validation_only"] = validation_only - observed["validation_label"] = validation_label - - module.run_strategy = fake_run_strategy - - with module.app.test_request_context("/precheck", method="POST"): - body, status = module.handle_precheck() - - self.assertEqual(status, 200) - self.assertEqual(body, "Precheck OK") - self.assertTrue(observed["force_run"]) - self.assertTrue(observed["validation_only"]) - self.assertEqual(observed["validation_label"], "precheck") - - def test_handle_dry_run_alias_forces_strategy_dry_run(self): + def test_handle_dry_run_forces_strategy_dry_run(self): module = load_module() observed = {"force_run": None, "validation_only": None} @@ -389,7 +364,7 @@ def fake_run_strategy(*, force_run=False, validation_only=False, validation_labe self.assertEqual(body, "Dry Run OK") self.assertTrue(observed["force_run"]) self.assertTrue(observed["validation_only"]) - self.assertEqual(observed["validation_label"], "precheck") + self.assertEqual(observed["validation_label"], "dry_run") def test_handle_probe_checks_account_snapshot_without_success_notification(self): module = load_module() @@ -644,7 +619,7 @@ def build_rebalance_config( self.assertTrue(observed["silent_cycle_notifications"]) self.assertEqual(observed["notification_title_key"], "") - def test_run_strategy_precheck_sets_precheck_notification_title(self): + def test_run_strategy_dry_run_sets_dry_run_notification_title(self): module = load_module() observed = {"notification_title_key": None} @@ -685,9 +660,9 @@ def build_rebalance_config( module.is_market_open_now = lambda **_kwargs: False module.run_rebalance_cycle = lambda **_kwargs: None - module.run_strategy(force_run=True, validation_only=True, validation_label="precheck") + module.run_strategy(force_run=True, validation_only=True, validation_label="dry_run") - self.assertEqual(observed["notification_title_key"], "precheck_title") + self.assertEqual(observed["notification_title_key"], "dry_run_title") def test_run_strategy_persists_machine_readable_report(self): module = load_module()