diff --git a/.github/workflows/execution-report-heartbeat.yml b/.github/workflows/execution-report-heartbeat.yml index 621256f..e493439 100644 --- a/.github/workflows/execution-report-heartbeat.yml +++ b/.github/workflows/execution-report-heartbeat.yml @@ -122,9 +122,9 @@ jobs: - name: Set up gcloud uses: google-github-actions/setup-gcloud@v3 - - name: Record and sync daily paper account snapshot + - name: Record and sync daily account snapshot id: account_history - if: ${{ !cancelled() && matrix.target.label == 'PAPER' && vars.ACCOUNT_HISTORY_RECORDING_ENABLED == 'true' }} + if: ${{ !cancelled() && contains(fromJSON('["PAPER","HK","SG"]'), matrix.target.label) && vars.ACCOUNT_HISTORY_RECORDING_ENABLED == 'true' }} continue-on-error: true env: ACCOUNT_HISTORY_RECORDING_ENABLED: ${{ vars.ACCOUNT_HISTORY_RECORDING_ENABLED }} diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index c117b35..0bbeeef 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -205,6 +205,7 @@ jobs: approved_candidate=0b939723c1db3ef59175535998b470cbcd4b8824 approved_http_snapshot_candidate=d8314a61df697cae1dd03a78ddc5c2fc4179ec67 approved_probe_snapshot_candidate=a2921d157efb887e9210fad6734ca040ea6e5293 + approved_sghk_snapshot_candidate=922f338fea7c46391b50bb8316ac88154596916f history_count=0 for history_value in \ "${ACCOUNT_HISTORY_RECORDING_ENABLED:-}" \ @@ -222,14 +223,29 @@ jobs: esac fi done - if [ "${history_count}" -ne 0 ] && [ "${WORKFLOW_TARGET}" != "PAPER" ]; then - echo "History settings are only admitted for PAPER." >&2 + if [ "${history_count}" -ne 0 ] && [ "${WORKFLOW_TARGET}" != "PAPER" ] \ + && [ "${WORKFLOW_TARGET}" != "HK" ] && [ "${WORKFLOW_TARGET}" != "SG" ]; then + echo "History settings are not admitted for this target." >&2 exit 1 fi if [ "${history_count}" -ne 0 ] && [ "${history_count}" -ne 4 ]; then echo "History settings are invalid." >&2 exit 1 fi + if [ "${history_count}" -eq 4 ] && [ "${WORKFLOW_TARGET}" != "PAPER" ]; then + case "${WORKFLOW_TARGET}" in + HK) expected_target=hk ;; + SG) expected_target=sg ;; + *) echo "History settings are not admitted for this target." >&2; exit 1 ;; + esac + if [ "${ACCOUNT_HISTORY_RECORDING_ENABLED}" != "true" ] \ + || [ "${ACCOUNT_HISTORY_TARGET_ID}" != "${expected_target}" ] \ + || [ "${ACCOUNT_HISTORY_EXPECTED_SCOPE}" != "${WORKFLOW_TARGET}" ] \ + || [ "${ACCOUNT_HISTORY_GCS_PREFIX}" != "gs://qsl-runtime-logs-shared/longbridge/account_snapshots" ]; then + echo "History settings do not match the approved account." >&2 + exit 1 + fi + fi snapshot_setting="${ACCOUNT_SNAPSHOT_ENABLED_INPUT:-}" if [ -n "${snapshot_setting}" ] \ && [ "${snapshot_setting}" != "true" ] \ @@ -257,6 +273,10 @@ jobs: && [ -z "${snapshot_setting}" ] \ && [ "${SOURCE_COMMIT}" = "${approved_probe_snapshot_candidate}" ]; then image_mode=probe + elif { [ "${WORKFLOW_TARGET}" = "HK" ] || [ "${WORKFLOW_TARGET}" = "SG" ]; } \ + && [ "${history_count}" -eq 4 ] && [ -z "${snapshot_setting}" ] \ + && [ "${SOURCE_COMMIT}" = "${approved_sghk_snapshot_candidate}" ]; then + image_mode=account-history else echo "Image source is not approved." >&2 exit 1 @@ -281,13 +301,13 @@ jobs: fi - name: Set up Python for image-only admission - if: inputs.target == 'PAPER' + if: inputs.target == 'PAPER' || inputs.target == 'HK' || inputs.target == 'SG' uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install frozen image-only admission dependencies - if: inputs.target == 'PAPER' + if: inputs.target == 'PAPER' || inputs.target == 'HK' || inputs.target == 'SG' run: | set -euo pipefail python -m pip install --upgrade pip uv @@ -311,6 +331,12 @@ jobs: approved_candidate=0b939723c1db3ef59175535998b470cbcd4b8824 approved_http_snapshot_candidate=d8314a61df697cae1dd03a78ddc5c2fc4179ec67 approved_probe_snapshot_candidate=a2921d157efb887e9210fad6734ca040ea6e5293 + approved_sghk_snapshot_candidate=922f338fea7c46391b50bb8316ac88154596916f + case "${WORKFLOW_TARGET}" in + HK) expected_sghk_target=hk ;; + SG) expected_sghk_target=sg ;; + *) expected_sghk_target="" ;; + esac history_count=0 for history_value in \ "${ACCOUNT_HISTORY_RECORDING_ENABLED:-}" \ @@ -335,6 +361,14 @@ jobs: && [ -z "${snapshot_setting}" ] \ && [ "${SOURCE_COMMIT}" = "${approved_probe_snapshot_candidate}" ]; then image_mode=probe + elif { [ "${WORKFLOW_TARGET}" = "HK" ] || [ "${WORKFLOW_TARGET}" = "SG" ]; } \ + && [ "${history_count}" -eq 4 ] && [ -z "${snapshot_setting}" ] \ + && [ "${ACCOUNT_HISTORY_RECORDING_ENABLED}" = "true" ] \ + && [ "${ACCOUNT_HISTORY_TARGET_ID}" = "${expected_sghk_target}" ] \ + && [ "${ACCOUNT_HISTORY_EXPECTED_SCOPE}" = "${WORKFLOW_TARGET}" ] \ + && [ "${ACCOUNT_HISTORY_GCS_PREFIX}" = "gs://qsl-runtime-logs-shared/longbridge/account_snapshots" ] \ + && [ "${SOURCE_COMMIT}" = "${approved_sghk_snapshot_candidate}" ]; then + image_mode=account-history else echo "Image source is not approved." >&2 exit 1 @@ -348,6 +382,9 @@ jobs: elif [ "${image_mode}" = "probe" ]; then archive_ref="${approved_probe_snapshot_candidate}" image_tag="${approved_probe_snapshot_candidate}" + elif [ "${image_mode}" = "account-history" ]; then + archive_ref="${approved_sghk_snapshot_candidate}" + image_tag="${approved_sghk_snapshot_candidate}" else archive_ref="${approved_candidate}" image_tag="${approved_candidate}" @@ -370,7 +407,7 @@ jobs: exit 1 fi fi - if [ "${WORKFLOW_TARGET}" = "PAPER" ]; then + if [ "${WORKFLOW_TARGET}" = "PAPER" ] || [ "${image_mode}" = "account-history" ]; then plan="$(mktemp)" uv run --no-sync python "${GITHUB_WORKSPACE}/scripts/verify_deployed_runtime_target_admission.py" \ --project="${GCP_PROJECT_ID}" \ @@ -409,7 +446,7 @@ jobs: if [ -n "${env_update_arg}" ]; then update_command+=(--update-env-vars="${env_update_arg}") fi - if [ "${WORKFLOW_TARGET}" = "PAPER" ]; then + if [ "${WORKFLOW_TARGET}" = "PAPER" ] || [ "${image_mode}" = "account-history" ]; then revision_suffix="r${GITHUB_RUN_ID}" revision_name="${CLOUD_RUN_SERVICE}-${revision_suffix}" if [[ ! "${GITHUB_RUN_ID}" =~ ^[0-9]+$ ]] \ @@ -420,7 +457,7 @@ jobs: update_command+=(--revision-suffix="${revision_suffix}") fi "${update_command[@]}" - if [ "${WORKFLOW_TARGET}" = "PAPER" ]; then + if [ "${WORKFLOW_TARGET}" = "PAPER" ] || [ "${image_mode}" = "account-history" ]; then uv run --no-sync python "${GITHUB_WORKSPACE}/scripts/verify_deployed_runtime_target_admission.py" \ --project="${GCP_PROJECT_ID}" \ --region="${CLOUD_RUN_REGION}" \ diff --git a/scripts/record_daily_account_snapshot.py b/scripts/record_daily_account_snapshot.py index e30a840..c847045 100644 --- a/scripts/record_daily_account_snapshot.py +++ b/scripts/record_daily_account_snapshot.py @@ -20,7 +20,8 @@ SNAPSHOT_SCHEMA = "longbridge_account_snapshot.v1" SOURCE_KIND = "deployment_scope_token_version" ACCOUNT_FACTS_SYNC_PATH = "/api/account-facts/sync" -EXPECTED_SCOPE = "PAPER" +_EXPECTED_TARGETS = {"paper": ("PAPER", "paper"), "hk": ("HK", "live"), "sg": ("SG", "live")} +_EXPECTED_GCS_PREFIX = "gs://qsl-runtime-logs-shared/longbridge/account_snapshots" SCHEDULER_SERVICE_ACCOUNT = "longbridge-platform-scheduler@longbridgequant.iam.gserviceaccount.com" OBSERVATION_WINDOW = timedelta(minutes=15) WAIT_SECONDS = 180.0 @@ -68,6 +69,7 @@ class _Config: bucket: str prefix_path: str target_id: str + expected_scope: str source_binding_id: str scheduler_job: str scheduler_resource: str @@ -154,21 +156,28 @@ def _config(env: Mapping[str, str]) -> _Config: target_id = str(env.get("ACCOUNT_HISTORY_TARGET_ID") or "").strip() expected_scope = str(env.get("ACCOUNT_HISTORY_EXPECTED_SCOPE") or "").strip() source_binding_id = str(env.get("ACCOUNT_HISTORY_EXPECTED_SOURCE_BINDING_ID") or "").strip() + target_contract = _EXPECTED_TARGETS.get(target_id) if ( _PROJECT_ID.fullmatch(project_id) is None - or target_id != "paper" + or project_id != "longbridgequant" + or target_contract is None or _TARGET_ID.fullmatch(target_id) is None - or expected_scope != EXPECTED_SCOPE + or expected_scope != target_contract[0] + or prefix != _EXPECTED_GCS_PREFIX or _BINDING_ID.fullmatch(source_binding_id) is None ): raise _Rejected("config_invalid") try: from application.runtime_target_manifest import load_runtime_target_manifest - matches = [target for target in load_runtime_target_manifest().targets if target.id == "paper"] + matches = [target for target in load_runtime_target_manifest().targets if target.id == target_id] except Exception: raise _Rejected("config_invalid") from None - if len(matches) != 1 or matches[0].mode != "paper": + if ( + len(matches) != 1 + or matches[0].mode != target_contract[1] + or matches[0].account_scope != expected_scope + ): raise _Rejected("config_invalid") service = matches[0].service region = matches[0].region @@ -186,6 +195,7 @@ def _config(env: Mapping[str, str]) -> _Config: bucket=bucket, prefix_path=prefix_path, target_id=target_id, + expected_scope=expected_scope, source_binding_id=source_binding_id, scheduler_job=scheduler_job, scheduler_resource=scheduler_resource, @@ -288,7 +298,7 @@ def _validate_scheduler_job(job: Mapping[str, Any], config: _Config) -> None: body_empty = body is None if ( job.get("name") != config.scheduler_resource - or job.get("state") != "ENABLED" + or job.get("state") not in ({"ENABLED", "PAUSED"} if config.target_id == "hk" else {"ENABLED"}) or target.get("httpMethod") != "POST" or target.get("uri") != f"{config.service_url}/probe" or not body_empty @@ -394,7 +404,7 @@ def _list_candidates( remaining = deadline - monotonic() if remaining <= 0: raise _Rejected("observation_timeout") - prefix = f"{config.prefix_path}/paper/{config.source_binding_id}/{day.isoformat()}/" + prefix = f"{config.prefix_path}/{config.target_id}/{config.source_binding_id}/{day.isoformat()}/" try: blobs = client.list_blobs( config.bucket, @@ -497,7 +507,7 @@ def _validate_history_object( set(payload) != expected_fields or payload.get("schema_version") != HISTORY_SCHEMA or payload.get("snapshot_schema_version") != SNAPSHOT_SCHEMA - or payload.get("account_scope") != EXPECTED_SCOPE + or payload.get("account_scope") != config.expected_scope or payload.get("target_id") != config.target_id or payload.get("snapshot_atomic") is not False or not isinstance(binding, Mapping) @@ -509,7 +519,7 @@ def _validate_history_object( started = _aware(payload.get("observed_started_at")) finished = _aware(payload.get("observed_finished_at")) expected_name = ( - f"{config.prefix_path}/paper/{config.source_binding_id}/" + f"{config.prefix_path}/{config.target_id}/{config.source_binding_id}/" f"{started.date().isoformat()}/{_filename_for(finished)}" ) if ( diff --git a/scripts/verify_deployed_runtime_target_admission.py b/scripts/verify_deployed_runtime_target_admission.py index f5cc7a0..db9343c 100644 --- a/scripts/verify_deployed_runtime_target_admission.py +++ b/scripts/verify_deployed_runtime_target_admission.py @@ -39,6 +39,8 @@ class AdmissionError(ValueError): APPROVED_PAPER_HTTP_SNAPSHOT_CANDIDATE = "d8314a61df697cae1dd03a78ddc5c2fc4179ec67" # Reviewed PAPER internal-probe snapshot producer. It carries no history or snapshot env update. APPROVED_PAPER_PROBE_SNAPSHOT_CANDIDATE = "a2921d157efb887e9210fad6734ca040ea6e5293" +# Reviewed read-only SG/HK account-snapshot producer. It is not a PAPER candidate. +APPROVED_SGHK_ACCOUNT_SNAPSHOT_CANDIDATE = "922f338fea7c46391b50bb8316ac88154596916f" # One already-staged PAPER revision that may hold history env while serving still runs an older image. _PAPER_HTTP_STAGED_SOURCE_REVISION = "longbridge-quant-paper-service-r36423178119" _PAPER_HTTP_STAGED_SOURCE_COMMIT = APPROVED_PAPER_HISTORY_CANDIDATE @@ -398,20 +400,26 @@ def _literal_values(configuration: Mapping[str, Any]) -> dict[str, str]: return {item["name"]: item["value"] for item in configuration["env"] if "value" in item} -def history_update(env: Mapping[str, str], *, workflow_target: str, project_id: str) -> dict[str, str] | None: - """Return an explicit PAPER history update, or None when the four inputs were omitted.""" +def history_update( + env: Mapping[str, str], *, workflow_target: str, project_id: str, allow_sghk: bool = False +) -> dict[str, str] | None: + """Return one exact target history update, or None when the four inputs were omitted.""" values = {key: str(env.get(key) or "") for key in _HISTORY_KEYS} if all(value == "" for value in values.values()): return None - if workflow_target != "PAPER": - raise AdmissionError("history settings are only admitted for PAPER") if any(not value or _UNSAFE_HISTORY.search(value) for value in values.values()): raise AdmissionError("history settings are invalid") if values["ACCOUNT_HISTORY_RECORDING_ENABLED"] != "true": raise AdmissionError("history settings are invalid") - if values["ACCOUNT_HISTORY_EXPECTED_SCOPE"] != "PAPER" or values["ACCOUNT_HISTORY_TARGET_ID"] != "paper": - raise AdmissionError("history settings are only admitted for PAPER") + target_pairs = {"PAPER": ("paper", "PAPER")} + if allow_sghk: + target_pairs.update({"HK": ("hk", "HK"), "SG": ("sg", "SG")}) + expected = target_pairs.get(workflow_target) + if expected is None: + raise AdmissionError("history settings are only admitted for approved targets") + if (values["ACCOUNT_HISTORY_TARGET_ID"], values["ACCOUNT_HISTORY_EXPECTED_SCOPE"]) != expected: + raise AdmissionError("history settings do not match the selected target") if _PROJECT_ID.fullmatch(project_id) is None or _TARGET_ID.fullmatch(values["ACCOUNT_HISTORY_TARGET_ID"]) is None: raise AdmissionError("history settings are invalid") try: @@ -420,9 +428,51 @@ def history_update(env: Mapping[str, str], *, workflow_target: str, project_id: raise AdmissionError("history settings are invalid") from None if prefix != values["ACCOUNT_HISTORY_GCS_PREFIX"]: raise AdmissionError("history settings are invalid") + if allow_sghk and prefix != "gs://qsl-runtime-logs-shared/longbridge/account_snapshots": + raise AdmissionError("history settings are not admitted for this candidate") return values +def _require_sghk_candidate_identity( + *, target_label: str, service: str, project: str, region: str, service_json: Mapping[str, Any] +) -> None: + expected = {"HK": ("hk", "HK"), "SG": ("sg", "SG")}.get(target_label) + if expected is None or project != "longbridgequant": + raise AdmissionError("candidate target does not match the approved account") + target_id, scope = expected + try: + from application.runtime_target_manifest import load_runtime_target_manifest + + matches = [item for item in load_runtime_target_manifest().targets if item.id == target_id] + except Exception: + raise AdmissionError("candidate target does not match the approved account") from None + if ( + len(matches) != 1 + or matches[0].mode != "live" + or matches[0].account_scope != scope + or matches[0].service != service + or matches[0].region != region + ): + raise AdmissionError("candidate target does not match the approved account") + env = _container_env(service_json) + try: + runtime_target = json.loads(env.get("RUNTIME_TARGET_JSON") or env.get("QSL_RUNTIME_TARGET_JSON") or "{}") + except json.JSONDecodeError: + raise AdmissionError("runtime target identity does not match the approved account") from None + selector = runtime_target.get("account_selector") if isinstance(runtime_target, Mapping) else None + selectors = (selector,) if isinstance(selector, str) else tuple(selector) if isinstance(selector, list) else None + deployment_selector = str(runtime_target.get("deployment_selector") or "") if isinstance(runtime_target, Mapping) else "" + if ( + not isinstance(runtime_target, Mapping) + or runtime_target.get("platform_id") != "longbridge" + or runtime_target.get("service_name") != service + or runtime_target.get("account_scope") != scope + or selectors != (scope,) + or deployment_selector != ("HK" if target_id == "hk" else "SG") + ): + raise AdmissionError("runtime target identity does not match the approved account") + + def _history_arg(values: Mapping[str, str] | None) -> str: if not values: return "" @@ -668,7 +718,16 @@ def prepare_image_only_staging( image_commit=image_commit, env=env, project=project ) admission = verify_service(service=service, service_json=service_json) - _require_paper_target_identity(service=service, service_json=service_json) + if image_commit == APPROVED_SGHK_ACCOUNT_SNAPSHOT_CANDIDATE: + _require_sghk_candidate_identity( + target_label=str(env.get("WORKFLOW_TARGET") or ""), + service=service, + project=project, + region=region, + service_json=service_json, + ) + else: + _require_paper_target_identity(service=service, service_json=service_json) try: traffic = serving_traffic_rows(service_json) except ReconcileError: @@ -746,7 +805,10 @@ def _validate_image_only_source( *, image_commit: str, env: Mapping[str, str], project: str ) -> tuple[dict[str, str] | None, str | None]: history = history_update( - env, workflow_target=str(env.get("WORKFLOW_TARGET") or ""), project_id=project + env, + workflow_target=str(env.get("WORKFLOW_TARGET") or ""), + project_id=project, + allow_sghk=image_commit == APPROVED_SGHK_ACCOUNT_SNAPSHOT_CANDIDATE, ) snapshot_value = _account_snapshot_update(env, workflow_target=str(env.get("WORKFLOW_TARGET") or "")) if image_commit == APPROVED_PAPER_HISTORY_CANDIDATE: @@ -762,6 +824,13 @@ def _validate_image_only_source( or snapshot_value is not None ): raise AdmissionError("image source is not approved") + elif image_commit == APPROVED_SGHK_ACCOUNT_SNAPSHOT_CANDIDATE: + if ( + str(env.get("WORKFLOW_TARGET") or "") not in {"HK", "SG"} + or history is None + or snapshot_value is not None + ): + raise AdmissionError("image source is not approved") elif _is_exact_main_image(image_commit, env): if history is not None: raise AdmissionError("image source is not approved") diff --git a/tests/test_daily_account_snapshot.py b/tests/test_daily_account_snapshot.py index 569389f..f037512 100644 --- a/tests/test_daily_account_snapshot.py +++ b/tests/test_daily_account_snapshot.py @@ -18,19 +18,25 @@ BINDING = "a" * 64 OTHER_BINDING = "b" * 64 SERVICE_URL = "https://longbridge-quant-paper-service-kcc3gcgmwq-de.a.run.app" -PREFIX = "gs://acct-history/longbridge/account_snapshots" +PREFIX = "gs://qsl-runtime-logs-shared/longbridge/account_snapshots" QRS_URL = "https://qrs.example.test/api/account-facts/sync" QRS_TOKEN = "synthetic-qrs-token" SECRET = "synthetic-secret-value" -def _env(**overrides): +def _env(target_id="paper", **overrides): + scope = {"paper": "PAPER", "hk": "HK", "sg": "SG"}[target_id] + service_url = { + "paper": SERVICE_URL, + "hk": "https://longbridge-quant-hk-service-kcc3gcgmwq-de.a.run.app", + "sg": "https://longbridge-quant-sg-service-kcc3gcgmwq-de.a.run.app", + }[target_id] result = { "ACCOUNT_HISTORY_RECORDING_ENABLED": "true", - "ACCOUNT_HISTORY_SERVICE_URL": SERVICE_URL, + "ACCOUNT_HISTORY_SERVICE_URL": service_url, "ACCOUNT_HISTORY_GCS_PREFIX": PREFIX, - "ACCOUNT_HISTORY_TARGET_ID": "paper", - "ACCOUNT_HISTORY_EXPECTED_SCOPE": "PAPER", + "ACCOUNT_HISTORY_TARGET_ID": target_id, + "ACCOUNT_HISTORY_EXPECTED_SCOPE": scope, "ACCOUNT_HISTORY_EXPECTED_SOURCE_BINDING_ID": BINDING, "GOOGLE_CLOUD_PROJECT": "longbridgequant", } @@ -38,16 +44,23 @@ def _env(**overrides): return result -def _job(**overrides): +def _job(target_id="paper", *, state="ENABLED", service_url=None, region=None, **overrides): + service = { + "paper": "longbridge-quant-paper-service", + "hk": "longbridge-quant-hk-service", + "sg": "longbridge-quant-sg-service", + }[target_id] + region = region or {"paper": "asia-east1", "hk": "asia-east2", "sg": "asia-southeast1"}[target_id] + service_url = service_url or _env(target_id)["ACCOUNT_HISTORY_SERVICE_URL"] job = { - "name": "projects/longbridgequant/locations/asia-east1/jobs/longbridge-quant-paper-service-probe-scheduler", - "state": "ENABLED", + "name": f"projects/longbridgequant/locations/{region}/jobs/{service}-probe-scheduler", + "state": state, "httpTarget": { "httpMethod": "POST", - "uri": f"{SERVICE_URL}/probe", + "uri": f"{service_url}/probe", "oidcToken": { "serviceAccountEmail": "longbridge-platform-scheduler@longbridgequant.iam.gserviceaccount.com", - "audience": SERVICE_URL, + "audience": service_url, }, }, "retryConfig": {"retryCount": 0, "maxRetryDuration": "0s"}, @@ -56,14 +69,14 @@ def _job(**overrides): return job -def _history(started=None, finished=None, *, binding=BINDING, balances=None, cash=None): +def _history(started=None, finished=None, *, target_id="paper", binding=BINDING, scope=None, balances=None, cash=None): started = started or T0 + timedelta(seconds=1) finished = finished or T0 + timedelta(seconds=2) return { "schema_version": "longbridge_account_snapshot_history.v1", "snapshot_schema_version": "longbridge_account_snapshot.v1", - "account_scope": "PAPER", - "target_id": "paper", + "account_scope": scope or {"paper": "PAPER", "hk": "HK", "sg": "SG"}[target_id], + "target_id": target_id, "source_binding": { "kind": "deployment_scope_token_version", "status": "bound", @@ -92,7 +105,7 @@ def _object(payload, *, path_date=None, raw=None, generation=7, size=None, error finished = datetime.fromisoformat(payload["observed_finished_at"]) day = path_date or started.date().isoformat() object_name = ( - f"longbridge/account_snapshots/paper/{BINDING}/{day}/" + f"longbridge/account_snapshots/{payload['target_id']}/{payload['source_binding']['id']}/{day}/" f"{finished.astimezone(timezone.utc).strftime('%H%M%S%fZ.json')}" ) raw_bytes = raw if raw is not None else json.dumps(payload, separators=(",", ":")).encode() @@ -150,7 +163,7 @@ def list_blobs(self, bucket, **kwargs): ) def bucket(self, bucket): - assert bucket == "acct-history" + assert bucket == "qsl-runtime-logs-shared" return self def blob(self, name, *, generation): @@ -312,6 +325,61 @@ def test_scheduler_zero_retry_protobuf_defaults_are_accepted(): assert [call[0] for call in spies.session.calls] == ["get", "post"] +@pytest.mark.parametrize(("target_id", "state"), [("hk", "PAUSED"), ("sg", "ENABLED")]) +def test_sghk_targets_use_exact_manifest_identity_and_publish_matching_history(target_id, state): + payload = _history(target_id=target_id) + job = _job(target_id, state=state) + spies = _Spies(objects=[_object(payload)], job=job) + result, spies = _record( + _env( + target_id, + ACCOUNT_FACTS_SYNC_ENABLED="true", + ACCOUNT_FACTS_SYNC_URL=QRS_URL, + ACCOUNT_FACTS_SYNC_TOKEN=QRS_TOKEN, + ), + spies, + ) + + assert result.status == "recorded" + assert result.publish_status == "published" + expected_region = {"hk": "asia-east2", "sg": "asia-southeast1"}[target_id] + expected_service = {"hk": "longbridge-quant-hk-service", "sg": "longbridge-quant-sg-service"}[target_id] + assert spies.session.calls[0][1] == ( + f"https://cloudscheduler.googleapis.com/v1/projects/longbridgequant/locations/" + f"{expected_region}/jobs/{expected_service}-probe-scheduler" + ) + assert spies.session.calls[1][1].endswith(f"/{expected_service}-probe-scheduler:run") + assert spies.storage.list_calls[0][1]["prefix"].startswith( + f"longbridge/account_snapshots/{target_id}/{BINDING}/" + ) + assert json.loads(spies.posts[0][1]["data"])["account_scope"] == {"hk": "HK", "sg": "SG"}[target_id] + + +def test_paused_hk_probe_is_the_only_paused_scheduler_accepted(): + hk_job = _job("hk", state="PAUSED") + hk_result, hk_spies = _record(_env("hk"), _Spies(objects=[_object(_history(target_id="hk"))], job=hk_job)) + assert hk_result.status == "recorded" + assert [call[0] for call in hk_spies.session.calls] == ["get", "post"] + + sg_job = _job("sg", state="PAUSED") + sg_result, sg_spies = _record(_env("sg"), _Spies(job=sg_job)) + assert sg_result.category == "scheduler_job_mismatch" + assert [call[0] for call in sg_spies.session.calls] == ["get"] + assert sg_spies.open_calls == [] + + +@pytest.mark.parametrize(("target_id", "wrong_scope"), [("hk", "SG"), ("sg", "HK")]) +def test_cross_scope_snapshot_is_never_published(monkeypatch, target_id, wrong_scope): + monkeypatch.setattr(snapshots, "WAIT_SECONDS", 1) + payload = _history(target_id=target_id, scope=wrong_scope) + result, spies = _record( + _env(target_id), + _Spies(objects=[_object(payload)], job=_job(target_id, state="PAUSED" if target_id == "hk" else "ENABLED")), + ) + assert result.category == "observation_timeout" + assert spies.posts == [] + + def test_unknown_scheduler_trigger_is_attempted_once_and_never_lists(): result, spies = _record(spies=_Spies(run_error=TimeoutError(SECRET))) assert result.category == "scheduler_run_unknown" diff --git a/tests/test_deployed_runtime_target_admission.py b/tests/test_deployed_runtime_target_admission.py index fe9f904..c5d9283 100644 --- a/tests/test_deployed_runtime_target_admission.py +++ b/tests/test_deployed_runtime_target_admission.py @@ -84,6 +84,8 @@ def test_verify_service_rejects_target_drift(target, profile, message): MAIN_SHA = "a" * 40 HTTP_SNAPSHOT_CANDIDATE = admission.APPROVED_PAPER_HTTP_SNAPSHOT_CANDIDATE PROBE_SNAPSHOT_CANDIDATE = admission.APPROVED_PAPER_PROBE_SNAPSHOT_CANDIDATE +SGHK_SNAPSHOT_CANDIDATE = admission.APPROVED_SGHK_ACCOUNT_SNAPSHOT_CANDIDATE +SGHK_PREFIX = "gs://qsl-runtime-logs-shared/longbridge/account_snapshots" STAGED_SOURCE_REVISION = "longbridge-quant-paper-service-r36423178119" STAGED_SOURCE_IMAGE = ( "asia-east1-docker.pkg.dev/synthetic-project/images/longbridgeplatform/paper-service" @@ -328,6 +330,151 @@ def run(command): assert "secret" not in json.dumps(plan) +def _sghk_target(target_label: str) -> dict: + target_id = target_label.lower() + scope = target_label + service = f"longbridge-quant-{target_id}-service" + return { + "platform_id": "longbridge", + "service_name": service, + "account_scope": scope, + "account_selector": scope, + "deployment_selector": "HK" if target_id == "hk" else "SG", + "strategy_profile": "tqqq_growth_income" if target_id == "hk" else "soxl_soxx_trend_income", + "execution_mode": "live", + "dry_run_only": False, + } + + +def _sghk_history(target_label: str) -> dict[str, str]: + target_id = target_label.lower() + return { + "WORKFLOW_TARGET": target_label, + "ACCOUNT_HISTORY_RECORDING_ENABLED": "true", + "ACCOUNT_HISTORY_GCS_PREFIX": SGHK_PREFIX, + "ACCOUNT_HISTORY_TARGET_ID": target_id, + "ACCOUNT_HISTORY_EXPECTED_SCOPE": target_label, + } + + +def _prepare_sghk(target_label: str, *, target_overrides=None, history_overrides=None, project="longbridgequant", region=None): + target = _sghk_target(target_label) + target.update(target_overrides or {}) + service = target["service_name"] + target_id = target_label.lower() + if region is None: + region = {"hk": "asia-east2", "sg": "asia-southeast1"}[target_id] + env = [ + {"name": "RUNTIME_TARGET_JSON", "value": json.dumps(target)}, + {"name": "STRATEGY_PROFILE", "value": target["strategy_profile"]}, + {"name": "LONGBRIDGE_DRY_RUN_ONLY", "value": "false"}, + {"name": "RUNTIME_TARGET_ENABLED", "value": "false"}, + ] + service_json = { + "metadata": {"annotations": {"run.googleapis.com/ingress": "internal"}}, + "spec": {"template": {"spec": {"serviceAccountName": "runtime@example.invalid", "containers": [{"env": env}]}}}, + "status": {"traffic": [{"revisionName": "serving-rev", "percent": 100}]}, + } + candidate_env = _sghk_history(target_label) + candidate_env.update(history_overrides or {}) + + def run(command): + if command[:3] == ["gcloud", "run", "revisions"]: + return json.dumps({ + "metadata": {"name": "serving-rev", "labels": {"commit-sha": SERVING}}, + "spec": {"serviceAccountName": "runtime@example.invalid", "containers": [{"env": env, "image": "serving-image"}]}, + "status": {"conditions": [{"type": "Ready", "status": "True"}]}, + }) + sha, name = command[2].split(":", 1) + return _declaration(name, UES) + + plan = admission.prepare_image_only_staging( + service=service, + project=project, + region=region, + service_json=service_json, + env=candidate_env, + image_commit=SGHK_SNAPSHOT_CANDIDATE, + run=run, + ) + return plan, candidate_env + + +@pytest.mark.parametrize("target_label", ["HK", "SG"]) +def test_sghk_snapshot_candidate_requires_exact_target_and_history_quartet(target_label): + plan, env = _prepare_sghk(target_label) + assert plan["image_commit"] == SGHK_SNAPSHOT_CANDIDATE + assert plan["history_values"]["ACCOUNT_HISTORY_TARGET_ID"] == target_label.lower() + assert plan["history_values"]["ACCOUNT_HISTORY_EXPECTED_SCOPE"] == target_label + assert plan["snapshot_value"] is None and plan["snapshot_arg"] == "" + assert plan["history_arg"] == ",".join( + f"{key}={env[key]}" for key in admission._HISTORY_KEYS + ) + assert plan["serving_traffic"] == [{"revisionName": "serving-rev", "percent": 100}] + + +@pytest.mark.parametrize( + ("target_label", "history_overrides", "target_overrides", "project", "region"), + [ + ("HK", {"ACCOUNT_HISTORY_TARGET_ID": "sg"}, None, "longbridgequant", "asia-east2"), + ("HK", {"ACCOUNT_HISTORY_EXPECTED_SCOPE": "SG"}, None, "longbridgequant", "asia-east2"), + ("HK", {"ACCOUNT_HISTORY_GCS_PREFIX": "gs://other-bucket/longbridge/account_snapshots"}, None, "longbridgequant", "asia-east2"), + ("SG", {"ACCOUNT_SNAPSHOT_ENABLED_INPUT": "false"}, None, "longbridgequant", "asia-southeast1"), + ("HK", None, {"account_scope": "SG"}, "longbridgequant", "asia-east2"), + ("HK", None, {"account_selector": "hk"}, "longbridgequant", "asia-east2"), + ("SG", None, {"deployment_selector": "sg"}, "longbridgequant", "asia-southeast1"), + ("SG", None, None, "other-project", "asia-southeast1"), + ("HK", None, None, "longbridgequant", "asia-east1"), + ], +) +def test_sghk_candidate_rejects_wrong_scope_selector_or_environment( + target_label, history_overrides, target_overrides, project, region +): + with pytest.raises(admission.AdmissionError): + _prepare_sghk( + target_label, + history_overrides=history_overrides, + target_overrides=target_overrides, + project=project, + region=region, + ) + + +def test_sghk_candidate_rejects_arbitrary_sha_and_template_drift(): + env = _sghk_history("HK") + with pytest.raises(admission.AdmissionError): + admission._validate_image_only_source(image_commit="b" * 40, env=env, project="longbridgequant") + + target = _sghk_target("HK") + service_json = { + "metadata": {"annotations": {"run.googleapis.com/ingress": "internal"}}, + "spec": {"template": {"spec": {"serviceAccountName": "runtime@example.invalid", "containers": [{"env": [ + {"name": "RUNTIME_TARGET_JSON", "value": json.dumps(target)}, + {"name": "STRATEGY_PROFILE", "value": target["strategy_profile"]}, + {"name": "LONGBRIDGE_DRY_RUN_ONLY", "value": "false"}, + {"name": "RUNTIME_TARGET_ENABLED", "value": "false"}, + ]}]}}}, + "status": {"traffic": [{"revisionName": "serving-rev", "percent": 100}]}, + } + drifted_env = list(service_json["spec"]["template"]["spec"]["containers"][0]["env"]) + drifted_env.append({"name": "UNEXPECTED", "value": "1"}) + + def run(command): + if command[:3] == ["gcloud", "run", "revisions"]: + return json.dumps({ + "metadata": {"name": "serving-rev", "labels": {"commit-sha": SERVING}}, + "spec": {"serviceAccountName": "runtime@example.invalid", "containers": [{"env": drifted_env, "image": "serving-image"}]}, + "status": {"conditions": [{"type": "Ready", "status": "True"}]}, + }) + return _declaration(command[2].split(":", 1)[1], UES) + + with pytest.raises(admission.AdmissionError, match="template does not match"): + admission.prepare_image_only_staging( + service=target["service_name"], project="longbridgequant", region="asia-east2", + service_json=service_json, env=env, image_commit=SGHK_SNAPSHOT_CANDIDATE, run=run, + ) + + @pytest.mark.parametrize("snapshot_setting", ["", "true", "false"]) def test_prepare_main_image_is_exact_workflow_sha_and_plans_only_snapshot_key(snapshot_setting): calls = [] diff --git a/tests/test_runtime_monitor_workflows.py b/tests/test_runtime_monitor_workflows.py index 9703dea..c448af3 100644 --- a/tests/test_runtime_monitor_workflows.py +++ b/tests/test_runtime_monitor_workflows.py @@ -123,7 +123,7 @@ def test_heartbeat_script_does_not_import_project_runtime_dependencies() -> None assert "from runtime_config_support import" not in script -def test_paper_snapshot_sync_uses_internal_probe_and_preserves_heartbeat_failure(): +def test_account_snapshot_sync_uses_matrix_identity_and_preserves_heartbeat_failure(): workflow = (ROOT / ".github/workflows/execution-report-heartbeat.yml").read_text() script = (ROOT / "scripts/record_daily_account_snapshot.py").read_text() account_step = workflow.index("id: account_history") @@ -134,7 +134,9 @@ def test_paper_snapshot_sync_uses_internal_probe_and_preserves_heartbeat_failure assert 'cron: "20 22 * * *"' in workflow assert gcloud_setup < account_step < heartbeat < final_failure assert "continue-on-error: true" in workflow[account_step:heartbeat] - assert "if: ${{ !cancelled() && matrix.target.label == 'PAPER' && vars.ACCOUNT_HISTORY_RECORDING_ENABLED == 'true' }}" in workflow + assert "contains(fromJSON('[\"PAPER\",\"HK\",\"SG\"]'), matrix.target.label)" in workflow + assert "ACCOUNT_HISTORY_TARGET_ID: ${{ matrix.target.id }}" in workflow + assert "ACCOUNT_HISTORY_EXPECTED_SCOPE: ${{ matrix.target.label }}" in workflow assert "ACCOUNT_HISTORY_EXPECTED_SOURCE_BINDING_ID: ${{ vars.ACCOUNT_HISTORY_EXPECTED_SOURCE_BINDING_ID }}" in workflow assert "matrix.target.service" not in workflow assert "matrix.target.region" not in workflow diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index c7163a0..6ffd9d2 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -448,6 +448,7 @@ assert "git archive ${{" not in job assert "0b939723c1db3ef59175535998b470cbcd4b8824" in job assert "d8314a61df697cae1dd03a78ddc5c2fc4179ec67" in job assert "a2921d157efb887e9210fad6734ca040ea6e5293" in job +assert "922f338fea7c46391b50bb8316ac88154596916f" in job assert '[ "${GITHUB_REPOSITORY:-}" != "QuantStrategyLab/LongBridgePlatform" ]' in job assert '[ "${SOURCE_COMMIT}" = "${GITHUB_SHA}" ] || [ "${SOURCE_COMMIT}" = "${approved_candidate}" ]' not in job for forbidden in ("sync_plan", "scheduler", "cleanup", "retire", "update-traffic"): @@ -476,6 +477,7 @@ with open(os.environ["STUB_LOG"], "a") as stream: approved = "0b939723c1db3ef59175535998b470cbcd4b8824" http_snapshot_candidate = "d8314a61df697cae1dd03a78ddc5c2fc4179ec67" probe_snapshot_candidate = "a2921d157efb887e9210fad6734ca040ea6e5293" +sghk_snapshot_candidate = "922f338fea7c46391b50bb8316ac88154596916f" staged_source_revision = "longbridge-quant-paper-service-r36423178119" staged_source_image = ( "asia-east1-docker.pkg.dev/synthetic-project/images/longbridgeplatform/synthetic-paper" @@ -508,19 +510,22 @@ def declaration(name, pin): def base_env(): service = os.environ["CLOUD_RUN_SERVICE"] - scope = os.environ.get("TARGET_ACCOUNT_SCOPE", "PAPER") + label = os.environ.get("WORKFLOW_TARGET", "PAPER") + scope = os.environ.get("TARGET_ACCOUNT_SCOPE", label) + profile = "tqqq_growth_income" if label == "HK" else "soxl_soxx_trend_income" if label == "SG" else "russell_top50_leader_rotation" target = { "platform_id": "longbridge", "service_name": service, "account_scope": scope, - "account_selector": ["PAPER"], - "strategy_profile": "russell_top50_leader_rotation", + "account_selector": [scope], + "deployment_selector": "HK" if label == "HK" else "SG" if label == "SG" else "PAPER", + "strategy_profile": profile, "execution_mode": "live", "dry_run_only": False, } return [ {"name": "RUNTIME_TARGET_JSON", "value": json.dumps(target)}, - {"name": "STRATEGY_PROFILE", "value": "russell_top50_leader_rotation"}, + {"name": "STRATEGY_PROFILE", "value": profile}, {"name": "LONGBRIDGE_DRY_RUN_ONLY", "value": "false"}, {"name": "RUNTIME_TARGET_ENABLED", "value": "true"}, ] @@ -569,7 +574,10 @@ def staged_source_revision_payload(): def staged_revision(): service = os.environ["CLOUD_RUN_SERVICE"] - image_repo = "registry.invalid/synthetic-project/synthetic-images/longbridgeplatform/" + service + image_repo = ( + "registry.invalid/" + os.environ["GCP_PROJECT_ID"] + "/" + + os.environ["GCP_ARTIFACT_REGISTRY_REPOSITORY"] + "/longbridgeplatform/" + service + ) env = base_env() history_keys = ( "ACCOUNT_HISTORY_RECORDING_ENABLED", @@ -604,11 +612,11 @@ def staged_revision(): if command == "git" and args == ["rev-parse", "HEAD"]: print(os.environ["CHECKOUT_SHA"]) -elif command == "git" and args[:4] == ["fetch", "--depth", "1", "origin"] and args[4] in (approved, http_snapshot_candidate, probe_snapshot_candidate): +elif command == "git" and args[:4] == ["fetch", "--depth", "1", "origin"] and args[4] in (approved, http_snapshot_candidate, probe_snapshot_candidate, sghk_snapshot_candidate): pass -elif command == "git" and args[:2] == ["cat-file", "-t"] and args[2] in (approved, http_snapshot_candidate, probe_snapshot_candidate): +elif command == "git" and args[:2] == ["cat-file", "-t"] and args[2] in (approved, http_snapshot_candidate, probe_snapshot_candidate, sghk_snapshot_candidate): print("commit") -elif command == "git" and args[:1] == ["rev-parse"] and len(args) == 2 and args[1] in (approved + "^{commit}", http_snapshot_candidate + "^{commit}", probe_snapshot_candidate + "^{commit}"): +elif command == "git" and args[:1] == ["rev-parse"] and len(args) == 2 and args[1] in (approved + "^{commit}", http_snapshot_candidate + "^{commit}", probe_snapshot_candidate + "^{commit}", sghk_snapshot_candidate + "^{commit}"): print(args[1].split("^", 1)[0]) elif command == "git" and args == ["archive", "HEAD"]: print("synthetic tracked source archive") @@ -618,11 +626,13 @@ elif command == "git" and args == ["archive", http_snapshot_candidate]: print("synthetic HTTP snapshot candidate archive") elif command == "git" and args == ["archive", probe_snapshot_candidate]: print("synthetic internal probe snapshot candidate archive") +elif command == "git" and args == ["archive", sghk_snapshot_candidate]: + print("synthetic SG/HK account snapshot candidate archive") elif command == "git" and args[:1] == ["show"] and len(args) == 2 and ":" in args[1]: sha, name = args[1].split(":", 1) if name not in ("uv.lock", "pyproject.toml", "qsl.toml"): raise SystemExit("unexpected git show") - if sha not in ("a" * 40, serving_sha, approved, http_snapshot_candidate, probe_snapshot_candidate): + if sha not in ("a" * 40, serving_sha, approved, http_snapshot_candidate, probe_snapshot_candidate, sghk_snapshot_candidate): raise SystemExit("admission read an unapproved source lock") pin = ("f" * 40) if sha == serving_sha and os.environ.get("BAD_SERVING_LOCK") == "1" else ues print(declaration(name, pin), end="") @@ -703,6 +713,7 @@ else: candidate = "0b939723c1db3ef59175535998b470cbcd4b8824" http_snapshot_candidate = "d8314a61df697cae1dd03a78ddc5c2fc4179ec67" probe_snapshot_candidate = "a2921d157efb887e9210fad6734ca040ea6e5293" + sghk_snapshot_candidate = "922f338fea7c46391b50bb8316ac88154596916f" staged_source_revision = "longbridge-quant-paper-service-r36423178119" staged_source_image = ( "asia-east1-docker.pkg.dev/synthetic-project/images/longbridgeplatform/" @@ -714,6 +725,10 @@ else: "ACCOUNT_HISTORY_TARGET_ID": "paper", "ACCOUNT_HISTORY_EXPECTED_SCOPE": "PAPER", } + sghk_history = { + "ACCOUNT_HISTORY_RECORDING_ENABLED": "true", + "ACCOUNT_HISTORY_GCS_PREFIX": "gs://qsl-runtime-logs-shared/longbridge/account_snapshots", + } def execute(**overrides): log.write_text("") @@ -723,13 +738,23 @@ else: count_path = root / "service-json-count" if count_path.exists(): count_path.unlink() - result = subprocess.run( - ["bash", "-c", "\n".join(blocks)], env={**base, **overrides}, - text=True, capture_output=True, cwd=root, - ) - if result.returncode: - print(result.stderr, file=sys.stderr) - return result.returncode, [json.loads(line) for line in log.read_text().splitlines()] + step_env_path = root / "github-env" + step_env_path.write_text("") + step_env = {**base, **overrides, "GITHUB_ENV": str(step_env_path)} + for block in blocks: + result = subprocess.run( + ["bash", "-c", block], env=step_env, + text=True, capture_output=True, cwd=root, + ) + if result.returncode: + print(result.stderr, file=sys.stderr) + return result.returncode, [json.loads(line) for line in log.read_text().splitlines()] + # GitHub exposes values appended to GITHUB_ENV only to later steps. + for line in step_env_path.read_text().splitlines(): + name, separator, value = line.partition("=") + if separator: + step_env[name] = value + return 0, [json.loads(line) for line in log.read_text().splitlines()] cases = 0 for label in ("PAPER", "HK", "SG"): @@ -869,6 +894,68 @@ else: code, calls = execute(**overrides) assert code != 0 and calls == [], overrides cases += 1 + for label, region in (("HK", "asia-east2"), ("SG", "asia-southeast1")): + target_history = { + **sghk_history, + "ACCOUNT_HISTORY_TARGET_ID": label.lower(), + "ACCOUNT_HISTORY_EXPECTED_SCOPE": label, + } + service = f"longbridge-quant-{label.lower()}-service" + code, calls = execute( + SOURCE_COMMIT=sghk_snapshot_candidate, + CHECKOUT_SHA="a" * 40, + WORKFLOW_TARGET=label, + CLOUD_RUN_SERVICE=service, + CLOUD_RUN_REGION=region, + GCP_PROJECT_ID="longbridgequant", + **target_history, + ) + assert code == 0, (label, code, calls) + updates = [call for call in calls if call[:4] == ["gcloud", "run", "services", "update"]] + image_repo = f"registry.invalid/longbridgequant/synthetic-images/longbridgeplatform/{service}" + assert len(updates) == 1 + assert "--no-traffic" in updates[0] + assert updates[0][-1] == "--revision-suffix=r123" + assert f"--update-labels=commit-sha={sghk_snapshot_candidate},github-run-id=123" in updates[0] + assert f"--image={image_repo}@{base['IMAGE_DIGEST']}" in updates[0] + assert "--update-env-vars=" + ",".join(f"{key}={value}" for key, value in target_history.items()) in updates[0] + assert ["git", "fetch", "--depth", "1", "origin", sghk_snapshot_candidate] in calls + assert ["git", "archive", sghk_snapshot_candidate] in calls + assert ["git", "archive", "HEAD"] not in calls + assert any(call[:2] == ["git", "show"] and call[2].startswith(sghk_snapshot_candidate + ":") for call in calls) + assert any(call[:3] == ["uv", "run", "--no-sync"] for call in calls) + assert not any("ACCOUNT_SNAPSHOT_ENABLED_INPUT" in part for call in calls for part in call) + cases += 1 + for overrides in ( + { + "SOURCE_COMMIT": sghk_snapshot_candidate, + "WORKFLOW_TARGET": "HK", + "CLOUD_RUN_SERVICE": "longbridge-quant-hk-service", + "CLOUD_RUN_REGION": "asia-east2", + "GCP_PROJECT_ID": "longbridgequant", + **{**sghk_history, "ACCOUNT_HISTORY_TARGET_ID": "sg", "ACCOUNT_HISTORY_EXPECTED_SCOPE": "HK"}, + }, + { + "SOURCE_COMMIT": sghk_snapshot_candidate, + "WORKFLOW_TARGET": "HK", + "CLOUD_RUN_SERVICE": "longbridge-quant-hk-service", + "CLOUD_RUN_REGION": "asia-east2", + "GCP_PROJECT_ID": "longbridgequant", + **{**sghk_history, "ACCOUNT_HISTORY_TARGET_ID": "hk", "ACCOUNT_HISTORY_EXPECTED_SCOPE": "SG"}, + }, + { + "SOURCE_COMMIT": sghk_snapshot_candidate, + "WORKFLOW_TARGET": "HK", + "CLOUD_RUN_SERVICE": "longbridge-quant-hk-service", + "CLOUD_RUN_REGION": "asia-east2", + "GCP_PROJECT_ID": "longbridgequant", + "ACCOUNT_SNAPSHOT_ENABLED_INPUT": "false", + **{**sghk_history, "ACCOUNT_HISTORY_TARGET_ID": "hk", "ACCOUNT_HISTORY_EXPECTED_SCOPE": "HK"}, + }, + ): + code, calls = execute(**overrides) + assert code != 0 and calls == [], overrides + cases += 1 for overrides in ( { "GITHUB_REF": "refs/heads/codex/natural-cycle-history-20260928",