From d504ddac51c7956b2d69e39df24f001cf854a2bd Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 19 May 2026 14:01:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=BE=A9=E5=B8=82=E5=A0=B4=E6=83=85?= =?UTF-8?q?=E5=A0=B1=20writer=20CLI=20lazy=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../market_intel_review_decision_writer.py | 5 ++- services/market_intel/__init__.py | 16 +++++++-- services/market_intel/phase.py | 3 ++ services/market_intel/service.py | 3 +- tests/test_market_intel_skeleton.py | 33 +++++++++++++++++++ 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 services/market_intel/phase.py diff --git a/scripts/market_intel_review_decision_writer.py b/scripts/market_intel_review_decision_writer.py index cb1d346..737c90e 100755 --- a/scripts/market_intel_review_decision_writer.py +++ b/scripts/market_intel_review_decision_writer.py @@ -19,11 +19,11 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) with contextlib.redirect_stdout(sys.stderr): - from services.market_intel import MarketIntelService # noqa: E402 from services.market_intel.candidate_queue_review_decision_writer_cli import ( # noqa: E402 APPROVAL_ENV_VAR, build_candidate_queue_review_decision_writer_cli_plan, ) + from services.market_intel.phase import MARKET_INTEL_PHASE # noqa: E402 def parse_args(argv=None): @@ -71,7 +71,6 @@ def _load_transaction_preview(path): def main(argv=None): args = parse_args(argv) - service = MarketIntelService() plan = build_candidate_queue_review_decision_writer_cli_plan( transaction_preview=_load_transaction_preview(args.transaction_json), execute_requested=args.execute, @@ -81,7 +80,7 @@ def main(argv=None): backup_verified=args.backup_verified, review_inventory_smoke_passed=args.review_inventory_smoke_passed, ) - plan["phase"] = service.phase + plan["phase"] = MARKET_INTEL_PHASE print(json.dumps(plan, ensure_ascii=False, indent=2, sort_keys=True)) return int(plan.get("exit_code", 2)) diff --git a/services/market_intel/__init__.py b/services/market_intel/__init__.py index 23f866f..05d1568 100644 --- a/services/market_intel/__init__.py +++ b/services/market_intel/__init__.py @@ -1,5 +1,17 @@ """跨平台市場情報服務模組。""" -from services.market_intel.service import MarketIntelService, MarketIntelRuntimeStatus - __all__ = ["MarketIntelService", "MarketIntelRuntimeStatus"] + + +def __getattr__(name): + if name in __all__: + from services.market_intel.service import ( + MarketIntelRuntimeStatus, + MarketIntelService, + ) + + return { + "MarketIntelService": MarketIntelService, + "MarketIntelRuntimeStatus": MarketIntelRuntimeStatus, + }[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/services/market_intel/phase.py b/services/market_intel/phase.py new file mode 100644 index 0000000..760cb2c --- /dev/null +++ b/services/market_intel/phase.py @@ -0,0 +1,3 @@ +"""市場情報 rollout phase 單一來源。""" + +MARKET_INTEL_PHASE = "phase_69_candidate_queue_review_decision_writer_cli" diff --git a/services/market_intel/service.py b/services/market_intel/service.py index 0a1f4e2..0552919 100644 --- a/services/market_intel/service.py +++ b/services/market_intel/service.py @@ -66,6 +66,7 @@ from services.market_intel.opportunity_scoring import ( ) from services.market_intel.platform_seed import build_platform_seed_rows from services.market_intel.platform_seed_db_diff import build_platform_seed_db_diff_plan +from services.market_intel.phase import MARKET_INTEL_PHASE from services.market_intel.scheduler_plan import build_scheduler_attach_plan from services.market_intel.platform_seed_writer import ( build_platform_seed_writer_plan, @@ -108,7 +109,7 @@ class MarketIntelRuntimeStatus: class MarketIntelService: """市場情報入口服務,先集中 feature gate 與安全狀態。""" - phase = "phase_69_candidate_queue_review_decision_writer_cli" + phase = MARKET_INTEL_PHASE def get_runtime_status(self) -> MarketIntelRuntimeStatus: return MarketIntelRuntimeStatus( diff --git a/tests/test_market_intel_skeleton.py b/tests/test_market_intel_skeleton.py index 6137b18..9f2aa13 100644 --- a/tests/test_market_intel_skeleton.py +++ b/tests/test_market_intel_skeleton.py @@ -7698,3 +7698,36 @@ def test_candidate_queue_writer_cli_script_outputs_blocked_gate(tmp_path): assert data["scheduler_attached"] is False assert data["transaction_preview_summary"]["statement_count"] == 1 assert "execute_requested" in data["blocked_reasons"] + + +def test_review_decision_writer_cli_script_outputs_blocked_gate_without_login_env(): + env = { + key: value + for key, value in os.environ.items() + if key not in {"LOGIN_PASSWORD", "SECRET_KEY"} + } + result = subprocess.run( + [sys.executable, "scripts/market_intel_review_decision_writer.py"], + capture_output=True, + check=False, + env=env, + text=True, + ) + data = json.loads(result.stdout) + + assert result.returncode == 0 + assert data["mode"] == "candidate_queue_review_decision_writer_cli_blocked" + assert data["phase"] == "phase_69_candidate_queue_review_decision_writer_cli" + assert data["execute_requested"] is False + assert data["apply_real_write_requested"] is False + assert data["approval_token_present"] is False + assert data["writer_implementation_enabled"] is False + assert data["ready_for_real_write"] is False + assert data["api_executes_cli"] is False + assert data["api_reads_approval_token"] is False + assert data["database_connection_opened"] is False + assert data["database_write_executed"] is False + assert data["database_commit_executed"] is False + assert data["scheduler_attached"] is False + assert data["writes_executed"] is False + assert data["exit_code"] == 0