From f719349c3935d70a29bd3da3962c34ca2f5d0205 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:54:12 +0800 Subject: [PATCH] fix(runtime): enforce Cloud Run service and tag budget --- .github/workflows/manual-strategy-switch.yml | 9 +- ...nual-strategy-switch-permission-control.md | 17 +++ ...trategy-switch-permission-control.zh-CN.md | 14 ++ python/scripts/build_runtime_switch.py | 11 +- python/scripts/cloud_run_name_budget.py | 54 +++++++ python/tests/test_cloud_run_name_budget.py | 132 ++++++++++++++++++ python/tests/test_runtime_settings.py | 74 +++++----- 7 files changed, 275 insertions(+), 36 deletions(-) create mode 100644 python/scripts/cloud_run_name_budget.py create mode 100644 python/tests/test_cloud_run_name_budget.py diff --git a/.github/workflows/manual-strategy-switch.yml b/.github/workflows/manual-strategy-switch.yml index b75333ca..0f384dd4 100644 --- a/.github/workflows/manual-strategy-switch.yml +++ b/.github/workflows/manual-strategy-switch.yml @@ -70,7 +70,7 @@ on: required: false type: string service_name: - description: "Runtime service name. blank = platform default." + description: "Exact service name; max 33 characters for cand-{sha8} (13). blank = platform default." required: false type: string plugin_mode: @@ -491,7 +491,12 @@ jobs: if [ "${SERVICE_TARGETS_MODE}" = "allow_create" ]; then args+=(--allow-create-service-target) fi - python3 python/scripts/build_runtime_switch.py "${args[@]}" + # The builder validates defaults, overrides and all retained inventory names. + # Forward only a successful cand-{sha8} (13-character tag) contract result. + if ! python3 python/scripts/build_runtime_switch.py "${args[@]}"; then + echo "::error::Switch target refused: check service_name syntax and the 46-character service+tag budget (cand tag: 13). No inventory was published." + exit 2 + fi python3 python/scripts/runtime_settings.py validate "${target_file}" echo "TARGET_FILE=${target_file}" >> "$GITHUB_ENV" diff --git a/docs/manual-strategy-switch-permission-control.md b/docs/manual-strategy-switch-permission-control.md index 454902de..9e81be25 100644 --- a/docs/manual-strategy-switch-permission-control.md +++ b/docs/manual-strategy-switch-permission-control.md @@ -64,3 +64,20 @@ verified. Single-operator mode removes the need for extra team reviewers, not investment authorization. See the Chinese note for the full checklist, fork guidance, and threat-model notes. + +## Cloud Run service naming + +The builder and manual workflow use a fixed `cand-{sha8}` tagged-URL contract: +the tag is 13 characters and service plus tag must fit within 46 characters. +Generated defaults and explicit `--service-name` overrides therefore need valid +lowercase service syntax and at most 33 characters. Names are preserved exactly +after the existing input whitespace trimming; no service is automatically renamed. + +Headroom below 1 is `TAG_IMPOSSIBLE`; headroom 1–12 is `TAG_TIGHT` for this +contract (including 9–12); both refuse generation before output or publication. +Every retained name alias in an emitted `CLOUD_RUN_SERVICE_TARGETS_JSON` inventory +is also checked. Existing long service identities require a separately approved +migration; passing this check proves only offline naming compatibility. Broker +`build_cloud_run_env_sync_plan.py` callers must validate their resolved exact name +under their own deployment contract; their code and live configuration are outside +this QRS change. diff --git a/docs/manual-strategy-switch-permission-control.zh-CN.md b/docs/manual-strategy-switch-permission-control.zh-CN.md index a7b5288f..47b634e3 100644 --- a/docs/manual-strategy-switch-permission-control.zh-CN.md +++ b/docs/manual-strategy-switch-permission-control.zh-CN.md @@ -108,3 +108,17 @@ - deployment branch 限制只允许 `main` - token 90 天轮换 - 独立记录每次切换的 Actions run URL + +## Cloud Run 服务命名 + +生成器和手动 workflow 使用固定的 `cand-{sha8}` 标签约定:标签长 13 个字符, +服务名与标签合计最多 46 个字符,因此生成的默认名和 `--service-name` 覆盖值 +均须符合小写服务名语法且最长 33 个字符。沿用输入首尾空白处理,保留服务名, +不会自动重命名服务。 + +标签剩余空间小于 1 时为 `TAG_IMPOSSIBLE`,1–12 时为此约定下的 `TAG_TIGHT` +(包括 9–12),两者均在生成输出或发布前拒绝。输出的 +`CLOUD_RUN_SERVICE_TARGETS_JSON` 中保留项的所有名称别名也接受检查。已有长名 +服务需要另行批准迁移;通过此检查只证明离线命名兼容。各 broker 的 +`build_cloud_run_env_sync_plan.py` 应按各自部署约定检查最终名称;其代码和线上 +配置不属于本次 QRS 修改范围。 diff --git a/python/scripts/build_runtime_switch.py b/python/scripts/build_runtime_switch.py index eb5a6cfa..b05ddef0 100644 --- a/python/scripts/build_runtime_switch.py +++ b/python/scripts/build_runtime_switch.py @@ -15,6 +15,8 @@ sys.path.insert(0, str(SCRIPT_DIR)) ROOT = SCRIPT_DIR.parents[1] +from cloud_run_name_budget import validate_service_inventory, validate_service_name # noqa: E402 + from runtime_settings import ( # noqa: E402 LIVE_CONTINUITY_STATES, SUPPORTED_PLATFORMS, @@ -826,6 +828,7 @@ def _build_runtime_target(args: argparse.Namespace) -> dict[str, Any]: ) account_selector = _split_csv(args.account_selector) or _account_selector_default(platform, account_scope) service_name = args.service_name.strip() if args.service_name else _default_service_name(platform, target_name) + validate_service_name(service_name) strategy_profile = args.strategy_profile.strip().lower() _validate_requested_execution_mode(strategy_profile, args.execution_mode) _validate_dca_platform(platform, strategy_profile) @@ -1100,6 +1103,9 @@ def build_switch_target(args: argparse.Namespace) -> dict[str, Any]: target["plugin_mounts"] = top_level_mounts if repository_variables: target["repository_variables"] = repository_variables + for variables in (extra_variables, repository_variables): + if "CLOUD_RUN_SERVICE_TARGETS_JSON" in variables: + validate_service_inventory(variables["CLOUD_RUN_SERVICE_TARGETS_JSON"]) errors = validate_target(target) if errors: raise ValueError("; ".join(errors)) @@ -1117,7 +1123,10 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument("--deployment-selector", default="") parser.add_argument("--account-selector", default="") parser.add_argument("--account-scope", default="") - parser.add_argument("--service-name", default="") + parser.add_argument( + "--service-name", default="", + help="Exact service name; must fit the 46-character budget with cand-{sha8} (13 characters).", + ) parser.add_argument("--execution-windows-json", default="") parser.add_argument( "--decision-data-json", diff --git a/python/scripts/cloud_run_name_budget.py b/python/scripts/cloud_run_name_budget.py new file mode 100644 index 00000000..7dbb0799 --- /dev/null +++ b/python/scripts/cloud_run_name_budget.py @@ -0,0 +1,54 @@ +"""Offline preflight for the fixed cand-{sha8} tagged-URL contract. + +Local Option B implementation: validate exact names, never rename or allocate tags. +The combined Cloud Run service + traffic tag budget is 46 characters. +""" +from __future__ import annotations + +import json +import re +from typing import Any + +SERVICE_TAG_BUDGET = 46 +CANDIDATE_TAG_LENGTH = 13 # cand- + eight hexadecimal commit characters + + +def validate_service_name(service_name: Any) -> None: + """Refuse invalid or over-budget names without exposing resource identities.""" + if not isinstance(service_name, str) or not re.fullmatch(r"[a-z](?:[a-z0-9-]*[a-z0-9])?", service_name): + raise ValueError("service_name must use lowercase ASCII letters, digits and internal hyphens") + headroom = SERVICE_TAG_BUDGET - len(service_name) + if headroom < CANDIDATE_TAG_LENGTH: + classification = "TAG_IMPOSSIBLE" if headroom < 1 else "TAG_TIGHT" + raise ValueError( + f"service_name {classification}: length={len(service_name)}, tag_headroom={headroom}; " + f"cand-{{sha8}} requires {CANDIDATE_TAG_LENGTH} characters within the " + f"{SERVICE_TAG_BUDGET}-character service+tag budget. " + "Inventory generation refused; existing services must not be silently renamed." + ) + + +def validate_service_inventory(value: Any) -> None: + """Check every retained name alias before a complete inventory is emitted.""" + if isinstance(value, str): + try: + value = json.loads(value) + except json.JSONDecodeError: + raise ValueError("service inventory must contain valid JSON") from None + entries = value.get("targets") if isinstance(value, dict) else value + if not isinstance(entries, list): + raise ValueError("service inventory targets must be an array") + for entry in entries: + if not isinstance(entry, dict): + raise ValueError("service inventory entries must be objects") + names = [entry[key] for key in ("service", "service_name", "cloud_run_service") if key in entry] + if "runtime_target" in entry: + runtime_target = entry["runtime_target"] + if not isinstance(runtime_target, dict): + raise ValueError("service inventory runtime_target must be an object") + if "service_name" in runtime_target: + names.append(runtime_target["service_name"]) + if not names: + raise ValueError("service inventory entry requires service_name") + for name in names: + validate_service_name(name) diff --git a/python/tests/test_cloud_run_name_budget.py b/python/tests/test_cloud_run_name_budget.py new file mode 100644 index 00000000..eb583ab9 --- /dev/null +++ b/python/tests/test_cloud_run_name_budget.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import contextlib +import io +import json +import os +import re +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" +sys.path.insert(0, str(SCRIPTS)) +import build_runtime_switch as switch + + +class CloudRunNameBudgetTest(unittest.TestCase): + def args(self, *extra, platform="ibkr", target="paper"): + return switch.build_parser().parse_args([ + "--platform", platform, "--target-name", target, + "--strategy-profile", "tqqq_growth_income", "--execution-mode", "dry_run", *extra, + ]) + + def test_impossible_override_refuses_without_echo_or_output(self): + name = "interactive-brokers-" + "x" * 20 + "-service" + self.assertEqual(len(name), 48) + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "target.json" + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + status = switch.main([ + "--platform", "ibkr", "--target-name", "paper", + "--strategy-profile", "tqqq_growth_income", "--execution-mode", "dry_run", + "--service-name", name, "--output", str(output), + ]) + self.assertEqual(status, 2) + self.assertFalse(output.exists()) + self.assertIn("TAG_IMPOSSIBLE", stderr.getvalue()) + self.assertNotIn(name, stderr.getvalue()) + + def test_tight_names_refuse_fixed_candidate_contract(self): + for length in (34, 37, 45): + with self.subTest(length=length), self.assertRaisesRegex(ValueError, "TAG_TIGHT"): + switch.build_switch_target(self.args("--service-name", "s" * length)) + + def test_generated_and_fixed_names_use_same_gate(self): + for platform in ("ibkr", "longbridge"): + with self.subTest(platform=platform), self.assertRaisesRegex(ValueError, "TAG_IMPOSSIBLE"): + switch.build_switch_target(self.args(platform=platform, target="x" * 30)) + with patch.dict(switch.DEFAULT_SERVICE_NAME, {"firstrade": "s" * 48}): + with self.assertRaisesRegex(ValueError, "TAG_IMPOSSIBLE"): + switch.build_switch_target(self.args(platform="firstrade")) + + def test_invalid_names_fail_without_echo(self): + for name in (" ", "Bad-Service", "-bad", "bad-", "bad_name", "é-service", "bad\nname"): + with self.subTest(name=name): + with self.assertRaisesRegex(ValueError, "service_name") as caught: + switch.build_switch_target(self.args("--service-name=" + name)) + if name.strip(): + self.assertNotIn(name, str(caught.exception)) + + def test_short_names_pass_unchanged_at_exact_boundary(self): + for name in ("s", "demo-service", "s" * 33): + with self.subTest(name=name): + target = switch.build_switch_target(self.args("--service-name", name)) + self.assertEqual(target["runtime_target"]["service_name"], name) + for platform in switch.SUPPORTED_PLATFORMS: + with self.subTest(platform=platform): + target = switch._build_runtime_target(self.args(platform=platform)) + self.assertLessEqual(len(target["service_name"]), 33) + + def test_retained_inventory_aliases_cannot_bypass_gate(self): + for alias in ("service", "service_name", "cloud_run_service", "runtime_target"): + bad = {alias: {"service_name": "s" * 48} if alias == "runtime_target" else "s" * 48} + with self.subTest(alias=alias), tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "inventory.json" + path.write_text(json.dumps({"targets": [bad]})) + before = path.read_bytes() + with self.assertRaisesRegex(ValueError, "TAG_IMPOSSIBLE"): + switch.build_switch_target(self.args( + "--service-name", "demo-service", "--allow-create-service-target", + "--existing-service-targets-json-file", str(path), + )) + self.assertEqual(path.read_bytes(), before) + + def test_workflow_only_forwards_validated_target(self): + root = SCRIPTS.parents[1] + workflow = (root / ".github/workflows/manual-strategy-switch.yml").read_text() + step = workflow.split(" - name: Build switch target\n", 1)[1].split(" - name:", 1)[0] + script = "\n".join(line[10:] for line in step.split(" run: |\n", 1)[1].splitlines()) + for name, status in (("s" * 48, 2), ("demo-service", 0)): + with self.subTest(status=status), tempfile.TemporaryDirectory() as directory: + env = {**os.environ, **{key: "" for key in re.findall(r"\$\{([A-Z_]+)", script)}} + env.update(PLATFORM="ibkr", TARGET_NAME="paper", STRATEGY_PROFILE="tqqq_growth_income", + EXECUTION_MODE="dry_run", PLUGIN_MODE="none", OPTION_OVERLAY_MODE="current", + VARIABLE_SCOPE="default", SERVICE_NAME=name, RUNNER_TEMP=directory, + GITHUB_ENV=str(Path(directory) / "env")) + result = subprocess.run(["bash", "-c", script], cwd=root, env=env, + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, status, result.stderr) + output = Path(directory) / "runtime-switch-target.json" + self.assertEqual(output.exists(), status == 0) + if status: + self.assertIn("::error::Switch target refused", result.stdout) + self.assertFalse(Path(env["GITHUB_ENV"]).exists()) + self.assertNotIn(name, result.stdout + result.stderr) + else: + self.assertIn("TARGET_FILE=", Path(env["GITHUB_ENV"]).read_text()) + self.assertEqual(json.loads(output.read_text())["runtime_target"]["service_name"], name) + + def test_separate_cli_processes_refuse_same_invalid_name(self): + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "target.json" + command = [sys.executable, str(SCRIPTS / "build_runtime_switch.py"), + "--platform", "ibkr", "--target-name", "paper", "--strategy-profile", + "tqqq_growth_income", "--execution-mode", "dry_run", "--service-name", + "s" * 48, "--output", str(output)] + processes = [subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + for _ in range(2)] + results = [(process, process.communicate(timeout=30)) for process in processes] + for process, (stdout, stderr) in results: + self.assertEqual(process.returncode, 2) + self.assertEqual(stdout, "") + self.assertIn("TAG_IMPOSSIBLE", stderr) + self.assertFalse(output.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/test_runtime_settings.py b/python/tests/test_runtime_settings.py index 5c6f0142..576c3c02 100644 --- a/python/tests/test_runtime_settings.py +++ b/python/tests/test_runtime_settings.py @@ -896,6 +896,7 @@ def test_confirmed_legacy_ibkr_profiles_are_continuity_eligible(self): [ "--platform", "ibkr", + "--service-name", "demo-service", "--target-name", f"legacy-{profile}", "--strategy-profile", @@ -1331,6 +1332,7 @@ def test_build_switch_target_sets_cash_only_execution_from_control_field(self): [ "--platform", "ibkr", + "--service-name", "demo-service", "--target-name", "ibkr-primary", "--strategy-profile", @@ -1351,14 +1353,14 @@ def test_extract_account_sync_controls_reads_ibkr_service_targets(self): "runtime_target": { "platform_id": "ibkr", "strategy_profile": "nasdaq_sp500_smart_dca", - "service_name": "interactive-brokers-demo-ibkr-dca-service", + "service_name": "ibkr-demo-ibkr-dca-service", "account_scope": "demo-ibkr-dca", }, "extra_variables": { "CLOUD_RUN_SERVICE_TARGETS_JSON": { "targets": [ { - "service": "interactive-brokers-demo-ibkr-dca-service", + "service": "ibkr-demo-ibkr-dca-service", "ACCOUNT_GROUP": "demo-ibkr-dca", "DCA_MODE": "smart", "DCA_BASE_INVESTMENT_USD": "500", @@ -1385,19 +1387,19 @@ def test_extract_account_sync_controls_prefers_exact_service_when_scope_is_share "runtime_target": { "platform_id": "ibkr", "strategy_profile": "nasdaq_sp500_smart_dca", - "service_name": "interactive-brokers-shared-b-service", + "service_name": "ibkr-shared-b-service", "account_scope": "shared-account", }, "extra_variables": { "CLOUD_RUN_SERVICE_TARGETS_JSON": { "targets": [ { - "service": "interactive-brokers-shared-a-service", + "service": "ibkr-shared-a-service", "ACCOUNT_GROUP": "shared-account", "DCA_MODE": "fixed", }, { - "service": "interactive-brokers-shared-b-service", + "service": "ibkr-shared-b-service", "ACCOUNT_GROUP": "shared-account", "DCA_MODE": "smart", }, @@ -2043,6 +2045,7 @@ def test_build_switch_target_preserves_dry_run_semantics_for_all_profile_domains parser.parse_args( [ "--platform", platform, + "--service-name", "demo-service", "--target-name", target_name, "--strategy-profile", profile, "--execution-mode", "dry_run", @@ -2068,6 +2071,7 @@ def test_every_default_buildable_dry_run_route_builds_a_valid_no_order_target(se parser.parse_args( [ "--platform", platform, + "--service-name", "demo-service", "--target-name", f"coverage-{platform}", "--strategy-profile", profile, "--execution-mode", "dry_run", @@ -2096,6 +2100,7 @@ def test_parked_snapshot_strategy_builds_only_with_a_verified_artifact_pair(self parser.parse_args( [ "--platform", platform, + "--service-name", "demo-service", "--target-name", f"snapshot-{platform}", "--strategy-profile", "hk_low_vol_dividend_quality_snapshot", "--execution-mode", "dry_run", @@ -2504,6 +2509,7 @@ def test_build_switch_target_sets_platform_cash_only_execution(self): [ "--platform", "ibkr", + "--service-name", "demo-service", "--target-name", "ibkr-primary", "--strategy-profile", @@ -2610,7 +2616,7 @@ def test_build_switch_target_preserves_market_signal_fields_in_service_targets_w existing = { "targets": [ { - "service": "interactive-brokers-demo-ibkr-dca-service", + "service": "ibkr-demo-ibkr-dca-service", "ACCOUNT_GROUP": "demo-ibkr-dca", "IBKR_MARKET_SIGNAL_HANDOFF_INDEX_URI": "gs://signals/index.json", "IBKR_MARKET_SIGNAL_REQUIRED": "true", @@ -2622,7 +2628,7 @@ def test_build_switch_target_preserves_market_signal_fields_in_service_targets_w "deployment_selector": "demo-ibkr-dca", "account_selector": ["DEMO_IBKR_DCA"], "account_scope": "demo-ibkr-dca", - "service_name": "interactive-brokers-demo-ibkr-dca-service", + "service_name": "ibkr-demo-ibkr-dca-service", "execution_mode": "live", }, }, @@ -2643,7 +2649,7 @@ def test_build_switch_target_preserves_market_signal_fields_in_service_targets_w "--account-selector", "DEMO_IBKR_DCA", "--service-name", - "interactive-brokers-demo-ibkr-dca-service", + "ibkr-demo-ibkr-dca-service", "--plugin-mode", "none", "--existing-service-targets-json-file", @@ -3185,7 +3191,7 @@ def test_build_switch_target_patches_ibkr_service_targets_json(self): "defaults": {"NOTIFY_LANG": "zh"}, "targets": [ { - "service": "interactive-brokers-demo-ibkr-tqqq-service", + "service": "ibkr-demo-ibkr-tqqq-service", "ACCOUNT_GROUP": "demo-ibkr-tqqq", "IBKR_MIN_RESERVED_CASH_USD": "150", "IBKR_RESERVED_CASH_RATIO": "0.03", @@ -3206,12 +3212,12 @@ def test_build_switch_target_patches_ibkr_service_targets_json(self): "deployment_selector": "demo-ibkr-tqqq", "account_selector": ["DEMO_IBKR_PRIMARY"], "account_scope": "demo-ibkr-tqqq", - "service_name": "interactive-brokers-demo-ibkr-tqqq-service", + "service_name": "ibkr-demo-ibkr-tqqq-service", "execution_mode": "live", }, }, { - "service": "interactive-brokers-demo-ibkr-soxl-service", + "service": "ibkr-demo-ibkr-soxl-service", "ACCOUNT_GROUP": "demo-ibkr-soxl", "runtime_target": { "platform_id": "ibkr", @@ -3220,7 +3226,7 @@ def test_build_switch_target_patches_ibkr_service_targets_json(self): "deployment_selector": "demo-ibkr-soxl", "account_selector": ["DEMO_IBKR_SOXL"], "account_scope": "demo-ibkr-soxl", - "service_name": "interactive-brokers-demo-ibkr-soxl-service", + "service_name": "ibkr-demo-ibkr-soxl-service", "execution_mode": "live", }, }, @@ -3241,7 +3247,7 @@ def test_build_switch_target_patches_ibkr_service_targets_json(self): "--account-selector", "DEMO_IBKR_PRIMARY", "--service-name", - "interactive-brokers-demo-ibkr-tqqq-service", + "ibkr-demo-ibkr-tqqq-service", "--existing-service-targets-json-file", str(path), ] @@ -3273,7 +3279,7 @@ def test_build_switch_target_prefers_exact_service_when_account_scope_is_shared( existing = { "targets": [ { - "service": "interactive-brokers-shared-a-service", + "service": "ibkr-shared-a-service", "ACCOUNT_GROUP": "shared-account", "runtime_target": { "platform_id": "ibkr", @@ -3282,12 +3288,12 @@ def test_build_switch_target_prefers_exact_service_when_account_scope_is_shared( "deployment_selector": "shared-a", "account_selector": ["SHARED_ACCOUNT"], "account_scope": "shared-account", - "service_name": "interactive-brokers-shared-a-service", + "service_name": "ibkr-shared-a-service", "execution_mode": "live", }, }, { - "service": "interactive-brokers-shared-b-service", + "service": "ibkr-shared-b-service", "ACCOUNT_GROUP": "shared-account", "runtime_target": { "platform_id": "ibkr", @@ -3296,7 +3302,7 @@ def test_build_switch_target_prefers_exact_service_when_account_scope_is_shared( "deployment_selector": "shared-b", "account_selector": ["SHARED_ACCOUNT"], "account_scope": "shared-account", - "service_name": "interactive-brokers-shared-b-service", + "service_name": "ibkr-shared-b-service", "execution_mode": "live", }, }, @@ -3318,7 +3324,7 @@ def test_build_switch_target_prefers_exact_service_when_account_scope_is_shared( "--account-scope", "shared-account", "--service-name", - "interactive-brokers-shared-b-service", + "ibkr-shared-b-service", "--existing-service-targets-json-file", str(path), ] @@ -3334,13 +3340,13 @@ def test_build_switch_target_prefers_exact_service_when_account_scope_is_shared( def test_build_switch_target_allow_create_appends_service_with_shared_scope(self): existing = [ { - "service": "interactive-brokers-shared-a-service", + "service": "ibkr-shared-a-service", "ACCOUNT_GROUP": "shared-account", "runtime_target": { "platform_id": "ibkr", "strategy_profile": "global_etf_rotation", "account_scope": "shared-account", - "service_name": "interactive-brokers-shared-a-service", + "service_name": "ibkr-shared-a-service", }, } ] @@ -3358,7 +3364,7 @@ def test_build_switch_target_allow_create_appends_service_with_shared_scope(self "--account-scope", "shared-account", "--service-name", - "interactive-brokers-shared-b-service", + "ibkr-shared-b-service", "--existing-service-targets-json-file", str(path), "--allow-create-service-target", @@ -3373,11 +3379,11 @@ def test_build_switch_target_allow_create_appends_service_with_shared_scope(self self.assertEqual(len(patched), 2) self.assertEqual( patched[0]["runtime_target"]["service_name"], - "interactive-brokers-shared-a-service", + "ibkr-shared-a-service", ) self.assertEqual( patched[1]["runtime_target"]["service_name"], - "interactive-brokers-shared-b-service", + "ibkr-shared-b-service", ) def test_service_target_selection_does_not_treat_scope_as_service_identity(self): @@ -3387,7 +3393,7 @@ def test_service_target_selection_does_not_treat_scope_as_service_identity(self) } entries = [ { - "service": "interactive-brokers-existing-service", + "service": "ibkr-existing-service", "ACCOUNT_GROUP": "shared-account", } ] @@ -3526,7 +3532,7 @@ def test_build_switch_target_can_clear_preserved_ibkr_reserved_cash_fields(self) existing = { "targets": [ { - "service": "interactive-brokers-demo-ibkr-tqqq-service", + "service": "ibkr-demo-ibkr-tqqq-service", "ACCOUNT_GROUP": "demo-ibkr-tqqq", "IBKR_MIN_RESERVED_CASH_USD": "150", "IBKR_RESERVED_CASH_RATIO": "0.03", @@ -3537,7 +3543,7 @@ def test_build_switch_target_can_clear_preserved_ibkr_reserved_cash_fields(self) "deployment_selector": "demo-ibkr-tqqq", "account_selector": ["DEMO_IBKR_PRIMARY"], "account_scope": "demo-ibkr-tqqq", - "service_name": "interactive-brokers-demo-ibkr-tqqq-service", + "service_name": "ibkr-demo-ibkr-tqqq-service", "execution_mode": "live", }, }, @@ -3558,7 +3564,7 @@ def test_build_switch_target_can_clear_preserved_ibkr_reserved_cash_fields(self) "--account-selector", "DEMO_IBKR_PRIMARY", "--service-name", - "interactive-brokers-demo-ibkr-tqqq-service", + "ibkr-demo-ibkr-tqqq-service", "--existing-service-targets-json-file", str(path), "--extra-variables-json", @@ -3578,7 +3584,7 @@ def test_build_switch_target_patches_ibkr_service_targets_with_soxl_plugin_mount existing = { "targets": [ { - "service": "interactive-brokers-demo-ibkr-tqqq-service", + "service": "ibkr-demo-ibkr-tqqq-service", "ACCOUNT_GROUP": "demo-ibkr-tqqq", "runtime_target": { "platform_id": "ibkr", @@ -3587,7 +3593,7 @@ def test_build_switch_target_patches_ibkr_service_targets_with_soxl_plugin_mount "deployment_selector": "demo-ibkr-tqqq", "account_selector": ["DEMO_IBKR_PRIMARY"], "account_scope": "demo-ibkr-tqqq", - "service_name": "interactive-brokers-demo-ibkr-tqqq-service", + "service_name": "ibkr-demo-ibkr-tqqq-service", "execution_mode": "live", }, "IBKR_STRATEGY_PLUGIN_MOUNTS_JSON": { @@ -3619,7 +3625,7 @@ def test_build_switch_target_patches_ibkr_service_targets_with_soxl_plugin_mount "--account-selector", "DEMO_IBKR_PRIMARY", "--service-name", - "interactive-brokers-demo-ibkr-tqqq-service", + "ibkr-demo-ibkr-tqqq-service", "--existing-service-targets-json-file", str(path), ] @@ -3637,7 +3643,7 @@ def test_build_switch_target_updates_nested_service_controls_without_stale_overr existing = { "targets": [ { - "service": "interactive-brokers-demo-ibkr-soxl-service", + "service": "ibkr-demo-ibkr-soxl-service", "ACCOUNT_GROUP": "demo-ibkr-soxl", "env": { "RUNTIME_TARGET_ENABLED": "false", @@ -3650,7 +3656,7 @@ def test_build_switch_target_updates_nested_service_controls_without_stale_overr "deployment_selector": "demo-ibkr-soxl", "account_selector": ["DEMO_IBKR_SOXL"], "account_scope": "demo-ibkr-soxl", - "service_name": "interactive-brokers-demo-ibkr-soxl-service", + "service_name": "ibkr-demo-ibkr-soxl-service", "execution_mode": "live", }, }, @@ -3671,7 +3677,7 @@ def test_build_switch_target_updates_nested_service_controls_without_stale_overr "--account-selector", "DEMO_IBKR_SOXL", "--service-name", - "interactive-brokers-demo-ibkr-soxl-service", + "ibkr-demo-ibkr-soxl-service", "--existing-service-targets-json-file", str(path), "--extra-variables-json", @@ -3697,6 +3703,7 @@ def test_build_switch_target_rejects_unknown_ibkr_service_target_by_default(self [ "--platform", "ibkr", + "--service-name", "demo-service", "--target-name", "new-account", "--strategy-profile", @@ -3766,6 +3773,7 @@ def test_build_switch_target_can_explicitly_append_ibkr_service_target(self): [ "--platform", "ibkr", + "--service-name", "demo-service", "--target-name", "new-account", "--strategy-profile",