Files
ewoooc/tests/test_ai_automation_smoke_service.py
ogt 157f0f98be
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
feat(smoke): monitor pchome decision closeout
2026-07-03 08:30:17 +08:00

1223 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
def _auto_policy_guard_history_check(status="ok"):
return {
"name": "PChome auto-policy authorization guard",
"status": status,
"summary": "PChome auto-policy guard 已自動讀回 no-write/no-secret package",
"details": {
"policy": "read_only_pchome_growth_auto_policy_db_apply_authorization_lane_guard",
"result": "WAITING_FOR_DB_APPLY_AUTHORIZATION_REQUEST_CLOSEOUT",
"guard_ready_count": 0,
"lane_guard_check_count": 12,
"lane_guard_pass_count": 11,
"lane_guard_waiting_count": 1,
"authorization_request_closeout_ready_count": 0,
"exact_request_payload_field_count": 10,
"machine_request_manifest_step_count": 6,
"lane_entry_requirement_count": 6,
"reads_secret_count": 0,
"executes_sql_count": 0,
"writes_database_count": 0,
"primary_human_gate_count": 0,
"manual_review_mode": "exception_only",
"machine_verifiable": True,
"ready_for_database_apply_now": False,
"issues_database_apply_authorization": False,
"next_machine_action": "collect_machine_fetch_evidence_for_authorization_guard",
},
}
def _auto_policy_decision_preflight_history_check(status="ok"):
return {
"name": "PChome auto-policy decision preflight",
"status": status,
"summary": "PChome auto-policy decision preflight 已用本機 machine evidence 自動通過",
"details": {
"policy": "read_only_pchome_growth_auto_policy_db_apply_authorization_decision_preflight",
"result": "DB_APPLY_AUTHORIZATION_DECISION_PREFLIGHT_READY",
"decision_preflight_ready_count": 1,
"authorization_lane_guard_ready_count": 1,
"decision_preflight_check_count": 12,
"decision_preflight_pass_count": 12,
"decision_preflight_waiting_count": 0,
"decision_input_requirement_count": 8,
"decision_rejection_reason_count": 10,
"machine_fetch_evidence_ready_count": 1,
"reads_secret_count": 0,
"executes_sql_count": 0,
"writes_database_count": 0,
"primary_human_gate_count": 0,
"manual_review_mode": "exception_only",
"payload_source": "local_contract_fixture",
"execute_fetch": True,
"outbound_network": False,
"business_data_source": False,
"ready_for_database_apply_now": False,
"issues_database_apply_authorization": False,
"next_machine_action": "continue_to_pchome_auto_policy_decision_closeout_lane",
},
}
def _auto_policy_decision_closeout_history_check(status="ok"):
return {
"name": "PChome auto-policy decision closeout",
"status": status,
"summary": "PChome auto-policy decision closeout 已自動完成",
"details": {
"policy": "read_only_pchome_growth_auto_policy_db_apply_authorization_decision_closeout",
"result": "DB_APPLY_AUTHORIZATION_DECISION_CLOSEOUT_READY",
"decision_closeout_ready_count": 1,
"decision_preflight_ready_count": 1,
"decision_closeout_check_count": 12,
"decision_closeout_pass_count": 12,
"decision_closeout_waiting_count": 0,
"post_apply_verifier_required_count": 1,
"same_run_truth_required_count": 1,
"reads_secret_count": 0,
"executes_sql_count": 0,
"writes_database_count": 0,
"primary_human_gate_count": 0,
"manual_review_mode": "exception_only",
"payload_source": "local_contract_fixture",
"execute_fetch": True,
"outbound_network": False,
"business_data_source": False,
"ready_for_database_apply_now": False,
"issues_database_apply_authorization": False,
"permits_future_authorization_decision_lane": True,
"next_machine_action": "continue_to_pchome_auto_policy_authorization_issuer_gate",
},
}
def test_event_router_smoke_reports_queued_deliveries(tmp_path, monkeypatch):
from services import ai_automation_metrics as metrics
from services import ai_automation_smoke_service as smoke
from services import event_router
queue_path = tmp_path / "failed_deliveries.jsonl"
queue_path.write_text('{"event_key":"a"}\n{"event_key":"b"}\n', encoding="utf-8")
monkeypatch.setattr(event_router, "_QUEUE_PATH", str(queue_path))
metrics.reset_for_tests()
metrics.record_event_router_dispatch(
tier="L1",
event_type="crawler_timeout",
delivered=False,
queued=True,
latency_ms=12,
)
result = smoke._event_router_check()
assert result["status"] == "warning"
assert result["details"]["queued_deliveries"] == 2
assert result["details"]["dispatch_metric_total"] == 1
assert result["details"]["dispatch_sync"] is True
def test_collect_ai_automation_smoke_uses_worst_status(monkeypatch):
from services import ai_automation_smoke_service as smoke
monkeypatch.setattr(smoke, "_event_router_check", lambda: smoke._check("event", "ok", "ok"))
monkeypatch.setattr(smoke, "_gemini_egress_check", lambda: smoke._check("gemini", "ok", "ok"))
monkeypatch.setattr(smoke, "_autoheal_check", lambda: smoke._check("autoheal", "warning", "warn"))
monkeypatch.setattr(smoke, "_nemotron_check", lambda: smoke._check("nemotron", "ok", "ok"))
monkeypatch.setattr(smoke, "_embedding_queue_check", lambda: smoke._check("embedding", "critical", "boom"))
monkeypatch.setattr(smoke, "_elephant_hitl_check", lambda: smoke._check("elephant", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_controlled_apply_drift_monitor_check", lambda: smoke._check("pchome", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_auto_policy_authorization_guard_check", lambda: smoke._check("auto-policy", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_auto_policy_decision_preflight_check", lambda: smoke._check("auto-policy preflight", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_auto_policy_decision_closeout_check", lambda: smoke._check("auto-policy closeout", "ok", "ok"))
monkeypatch.setattr(smoke, "_ai_surface_html_readback_check", lambda: smoke._check("surface", "ok", "ok"))
monkeypatch.setattr(smoke, "_sitewide_ui_ux_agent_check", lambda: smoke._check("sitewide", "ok", "ok"))
monkeypatch.setattr(smoke, "_sitewide_visual_qa_check", lambda: smoke._check("visual", "ok", "ok"))
result = smoke.collect_ai_automation_smoke(record_history=False)
assert result["status"] == "critical"
assert result["summary"] == {"ok": 11, "warning": 1, "critical": 1, "total": 13}
def test_pchome_controlled_apply_drift_monitor_reports_verified_zero_drift(monkeypatch):
from services import ai_automation_smoke_service as smoke
from services import pchome_mapping_backlog_service as backlog
class FakeEngine:
disposed = False
def dispose(self):
self.disposed = True
fake_engine = FakeEngine()
monkeypatch.setattr(smoke, "_create_pchome_drift_monitor_engine", lambda _path: fake_engine)
monkeypatch.setattr(
backlog,
"build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_receipt_replay_package",
lambda **_kwargs: {
"result": "DIRECT_MAPPING_RETRY_EXCEPTION_CONTROLLED_APPLY_RECEIPT_REPLAYED",
"summary": {
"target_selector_count": 4,
"post_apply_readback_pass_count": 4,
"executor_receipt_hash_match_count": 1,
},
},
)
monkeypatch.setattr(
backlog,
"build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_drift_verifier_package",
lambda **_kwargs: {
"result": "DIRECT_MAPPING_RETRY_EXCEPTION_CONTROLLED_APPLY_DRIFT_VERIFIED",
"summary": {
"target_selector_count": 4,
"post_apply_readback_pass_count": 4,
"drift_count": 0,
"drift_verified_count": 1,
"drift_verifier_artifact_materialized_count": 1,
"drift_verifier_artifact_hash_match_count": 1,
"writes_database_count": 0,
},
},
)
result = smoke._pchome_controlled_apply_drift_monitor_check()
assert result["status"] == "ok"
assert result["details"]["selector_count"] == 4
assert result["details"]["drift_count"] == 0
assert result["details"]["writes_database"] is False
assert result["details"]["materialize_artifacts"] is False
assert fake_engine.disposed is True
def test_pchome_auto_policy_authorization_guard_reports_no_write_exception_only(monkeypatch):
from services import ai_automation_smoke_service as smoke
from services import pchome_mapping_backlog_service as backlog
from services import pchome_revenue_growth_service as growth
class FakeEngine:
disposed = False
def dispose(self):
self.disposed = True
fake_engine = FakeEngine()
monkeypatch.setattr(smoke, "_create_pchome_drift_monitor_engine", lambda _path: fake_engine)
monkeypatch.setattr(growth, "build_pchome_growth_opportunities", lambda _engine, limit: {"success": True})
monkeypatch.setattr(
backlog,
"build_pchome_auto_policy_db_apply_authorization_lane_guard",
lambda *_args, **_kwargs: {
"policy": "read_only_pchome_growth_auto_policy_db_apply_authorization_lane_guard",
"result": "WAITING_FOR_DB_APPLY_AUTHORIZATION_REQUEST_CLOSEOUT",
"source_policy": "read_only_pchome_growth_auto_policy_db_apply_authorization_request_closeout",
"summary": {
"authorization_lane_guard_ready_count": 0,
"authorization_request_closeout_ready_count": 0,
"lane_guard_check_count": 12,
"lane_guard_pass_count": 11,
"lane_guard_waiting_count": 1,
"exact_request_payload_field_count": 10,
"machine_request_manifest_step_count": 6,
"lane_entry_requirement_count": 6,
"reads_secret_count": 0,
"executes_sql_count": 0,
"writes_database_count": 0,
"primary_human_gate_count": 0,
"manual_review_required_count": 0,
},
"future_authorization_lane_guard": {
"ready_for_database_apply_now": False,
"issues_database_apply_authorization": False,
"requires_fresh_production_truth_in_same_run": True,
},
"lane_transfer_contract": {
"machine_verifiable": True,
"writes_database": False,
"issues_database_apply_authorization": False,
},
"safety": {
"reads_secret_in_preview": False,
"executes_sql": False,
"writes_database": False,
"manual_review_mode": "exception_only",
},
},
)
result = smoke._pchome_auto_policy_authorization_guard_check()
assert result["status"] == "ok"
assert result["details"]["lane_guard_pass_count"] == 11
assert result["details"]["primary_human_gate_count"] == 0
assert result["details"]["writes_database_count"] == 0
assert result["details"]["next_machine_action"] == "collect_machine_fetch_evidence_for_authorization_guard"
assert fake_engine.disposed is True
def test_pchome_auto_policy_decision_preflight_reports_machine_fetch_no_write(monkeypatch):
from services import ai_automation_smoke_service as smoke
from services import pchome_mapping_backlog_service as backlog
captured = {}
def fake_preflight(*_args, **kwargs):
captured.update(kwargs)
return {
"policy": "read_only_pchome_growth_auto_policy_db_apply_authorization_decision_preflight",
"result": "DB_APPLY_AUTHORIZATION_DECISION_PREFLIGHT_READY",
"source_policy": "read_only_pchome_growth_auto_policy_db_apply_authorization_lane_guard",
"summary": {
"authorization_decision_preflight_ready_count": 1,
"authorization_lane_guard_ready_count": 1,
"decision_preflight_check_count": 12,
"decision_preflight_pass_count": 12,
"decision_preflight_waiting_count": 0,
"decision_input_requirement_count": 8,
"decision_rejection_reason_count": 10,
"lane_entry_requirement_count": 6,
"exact_request_payload_field_count": 10,
"machine_request_manifest_step_count": 6,
"reads_secret_count": 0,
"executes_sql_count": 0,
"writes_database_count": 0,
"primary_human_gate_count": 0,
"manual_review_required_count": 0,
},
"future_authorization_decision_preflight": {
"ready_for_future_authorization_decision": True,
"ready_for_database_apply_now": False,
"issues_database_apply_authorization": False,
"requires_fresh_production_truth_in_same_run": True,
},
"decision_preflight_envelope": {
"allows_authorization_decision_in_future_lane": True,
"issues_database_apply_authorization": False,
"ready_for_database_apply_now": False,
},
"safety": {
"reads_secret_in_preview": False,
"executes_sql": False,
"writes_database": False,
"manual_review_mode": "exception_only",
},
}
monkeypatch.setattr(
backlog,
"build_pchome_auto_policy_db_apply_authorization_decision_preflight",
fake_preflight,
)
result = smoke._pchome_auto_policy_decision_preflight_check()
assert result["status"] == "ok"
assert captured["execute_fetch"] is True
assert captured["http_get"] is smoke._pchome_auto_policy_machine_fetch_evidence_get
assert result["details"]["decision_preflight_pass_count"] == 12
assert result["details"]["machine_fetch_evidence_ready_count"] == 1
assert result["details"]["machine_fetch_evidence_source"] == "local_schema_fixture"
assert result["details"]["payload_source"] == "local_contract_fixture"
assert result["details"]["outbound_network"] is False
assert result["details"]["business_data_source"] is False
assert result["details"]["primary_human_gate_count"] == 0
assert result["details"]["writes_database_count"] == 0
assert result["details"]["next_machine_action"] == "continue_to_pchome_auto_policy_decision_closeout_lane"
def test_pchome_auto_policy_decision_closeout_reports_no_write_package(monkeypatch):
from services import ai_automation_smoke_service as smoke
from services import pchome_mapping_backlog_service as backlog
captured = {}
def fake_closeout(*_args, **kwargs):
captured.update(kwargs)
return {
"policy": "read_only_pchome_growth_auto_policy_db_apply_authorization_decision_closeout",
"result": "DB_APPLY_AUTHORIZATION_DECISION_CLOSEOUT_READY",
"source_policy": "read_only_pchome_growth_auto_policy_db_apply_authorization_decision_preflight",
"summary": {
"authorization_decision_closeout_ready_count": 1,
"authorization_decision_preflight_ready_count": 1,
"decision_closeout_check_count": 12,
"decision_closeout_pass_count": 12,
"decision_closeout_waiting_count": 0,
"decision_input_requirement_count": 8,
"decision_rejection_reason_count": 10,
"post_apply_verifier_required_count": 1,
"same_run_truth_required_count": 1,
"reads_secret_count": 0,
"executes_sql_count": 0,
"writes_database_count": 0,
"primary_human_gate_count": 0,
"manual_review_required_count": 0,
},
"future_authorization_decision_closeout": {
"ready_for_future_authorization_decision_closeout": True,
"ready_for_database_apply_now": False,
"issues_database_apply_authorization": False,
},
"future_authorization_decision_package": {
"ready_for_future_authorization_decision_package": True,
"ready_for_database_apply_now": False,
"issues_database_apply_authorization": False,
"requires_post_apply_verifier": True,
"requires_fresh_production_truth_in_same_run": True,
},
"decision_closeout_contract": {
"machine_verifiable": True,
"permits_future_authorization_decision_lane": True,
"issues_database_apply_authorization": False,
"ready_for_database_apply_now": False,
"writes_database": False,
},
"safety": {
"reads_secret_in_preview": False,
"executes_sql": False,
"writes_database": False,
"manual_review_mode": "exception_only",
},
}
monkeypatch.setattr(
backlog,
"build_pchome_auto_policy_db_apply_authorization_decision_closeout",
fake_closeout,
)
result = smoke._pchome_auto_policy_decision_closeout_check()
assert result["status"] == "ok"
assert captured["execute_fetch"] is True
assert captured["http_get"] is smoke._pchome_auto_policy_machine_fetch_evidence_get
assert result["details"]["decision_closeout_pass_count"] == 12
assert result["details"]["post_apply_verifier_required_count"] == 1
assert result["details"]["same_run_truth_required_count"] == 1
assert result["details"]["payload_source"] == "local_contract_fixture"
assert result["details"]["outbound_network"] is False
assert result["details"]["business_data_source"] is False
assert result["details"]["primary_human_gate_count"] == 0
assert result["details"]["writes_database_count"] == 0
assert result["details"]["issues_database_apply_authorization"] is False
assert result["details"]["next_machine_action"] == "continue_to_pchome_auto_policy_authorization_issuer_gate"
def test_collect_ai_automation_smoke_persists_recent_history(tmp_path, monkeypatch):
from services import ai_automation_smoke_service as smoke
history_path = tmp_path / "smoke_history.jsonl"
monkeypatch.setattr(smoke, "_HISTORY_PATH", str(history_path))
monkeypatch.setattr(smoke, "_HISTORY_LIMIT", 2)
monkeypatch.setattr(smoke, "_event_router_check", lambda: smoke._check("event", "ok", "ok"))
monkeypatch.setattr(smoke, "_gemini_egress_check", lambda: smoke._check("gemini", "ok", "ok"))
monkeypatch.setattr(smoke, "_autoheal_check", lambda: smoke._check("autoheal", "ok", "ok"))
monkeypatch.setattr(smoke, "_nemotron_check", lambda: smoke._check("nemotron", "ok", "ok"))
monkeypatch.setattr(smoke, "_embedding_queue_check", lambda: smoke._check("embedding", "ok", "ok"))
monkeypatch.setattr(smoke, "_elephant_hitl_check", lambda: smoke._check("elephant", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_controlled_apply_drift_monitor_check", lambda: smoke._check("pchome", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_auto_policy_authorization_guard_check", lambda: smoke._check("auto-policy", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_auto_policy_decision_preflight_check", lambda: smoke._check("auto-policy preflight", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_auto_policy_decision_closeout_check", lambda: smoke._check("auto-policy closeout", "ok", "ok"))
monkeypatch.setattr(smoke, "_ai_surface_html_readback_check", lambda: smoke._check("surface", "ok", "ok"))
monkeypatch.setattr(smoke, "_sitewide_ui_ux_agent_check", lambda: smoke._check("sitewide", "ok", "ok"))
monkeypatch.setattr(smoke, "_sitewide_visual_qa_check", lambda: smoke._check("visual", "ok", "ok"))
first = smoke.collect_ai_automation_smoke(history_limit=5)
second = smoke.collect_ai_automation_smoke(history_limit=5)
third = smoke.collect_ai_automation_smoke(history_limit=5)
assert first["status"] == "ok"
assert second["history"]["counts"]["ok"] == 2
assert third["history"]["counts"]["ok"] == 2
assert len(history_path.read_text(encoding="utf-8").strip().splitlines()) == 2
def test_smoke_history_export_and_clear(tmp_path, monkeypatch):
from services import ai_automation_smoke_service as smoke
history_path = tmp_path / "smoke_history.jsonl"
history_path.write_text(
'{"generated_at":"2026-04-29T01:00:00","status":"ok"}\n'
'{"generated_at":"2026-04-29T02:00:00","status":"warning"}\n',
encoding="utf-8",
)
monkeypatch.setattr(smoke, "_HISTORY_PATH", str(history_path))
export = smoke.export_smoke_history_jsonl()
cleared = smoke.clear_smoke_history()
assert export["count"] == 2
assert '"status":"warning"' in export["content"]
assert cleared["cleared"] == 2
assert not history_path.exists()
def test_smoke_history_daily_summary():
from services import ai_automation_smoke_service as smoke
summary = smoke._history_summary([
{"generated_at": "2026-04-28T23:00:00", "status": "ok"},
{"generated_at": "2026-04-29T01:00:00", "status": "warning"},
{"generated_at": "2026-04-29T02:00:00", "status": "critical"},
])
assert summary["daily"] == [
{"date": "2026-04-28", "ok": 1, "warning": 0, "critical": 0, "total": 1},
{"date": "2026-04-29", "ok": 0, "warning": 1, "critical": 1, "total": 2},
]
def test_scheduled_automation_health_summary_reads_history_without_side_effects(tmp_path, monkeypatch):
from datetime import datetime
from services import ai_automation_smoke_service as smoke
history_path = tmp_path / "smoke_history.jsonl"
history_path.write_text(
json.dumps({
"generated_at": datetime.now().isoformat(timespec="seconds"),
"status": "ok",
"summary": {"ok": 13, "warning": 0, "critical": 0, "total": 13},
"checks": [
{
"name": "PChome 受控落地 drift monitor",
"status": "ok",
"summary": "PChome controlled apply drift 已驗證 4/4目前 0 drift",
"details": {
"result": "DIRECT_MAPPING_RETRY_EXCEPTION_CONTROLLED_APPLY_DRIFT_VERIFIED",
"source_receipt_replay_result": "DIRECT_MAPPING_RETRY_EXCEPTION_CONTROLLED_APPLY_RECEIPT_REPLAYED",
"selector_count": 4,
"readback_pass_count": 4,
"drift_count": 0,
},
},
_auto_policy_guard_history_check(),
_auto_policy_decision_preflight_history_check(),
_auto_policy_decision_closeout_history_check(),
{
"name": "AI surface HTML readback",
"status": "ok",
"summary": "AI surface HTML readback 通過 10/10無產品面 raw wording",
"details": {
"policy": "read_only_ai_surface_html_readback_v1",
"checked_surface_count": 10,
"pass_count": 10,
"failed_count": 0,
"forbidden_leak_count": 0,
},
},
{
"name": "Sitewide UI/UX Agent readback",
"status": "ok",
"summary": "Sitewide UI/UX Agent ok/no-op整站專業化 surface 無退化",
"details": {
"policy": "read_only_sitewide_ui_ux_agent_inventory_v1",
"template_count": 55,
"high_priority_template_count": 31,
"compact_guardrail_count": 29,
"issue_surface_count": 0,
"raw_engineering_issue_count": 0,
"repair_package_status": "no_op",
"repair_action_count": 0,
},
},
{
"name": "Sitewide visual QA readback",
"status": "ok",
"summary": "Sitewide visual QA ok12 routes x 3 viewports 無 overflow 退化",
"details": {
"policy": "read_only_sitewide_visual_qa_readback_v1",
"result_count": 36,
"route_count": 12,
"viewport_count": 3,
"failed_count": 0,
"overflow_issue_count": 0,
"visual_offender_count": 0,
"artifact_path": "data/ai_automation/sitewide_visual_qa_latest.json",
"artifact_generated_at": datetime.now().isoformat(timespec="seconds"),
"stale": False,
},
}
],
}, ensure_ascii=False) + "\n",
encoding="utf-8",
)
monkeypatch.setattr(smoke, "_HISTORY_PATH", str(history_path))
summary = smoke.build_scheduled_automation_health_summary(
history_limit=5,
freshness_max_age_hours=9999,
)
pchome_family = next(
item for item in summary["families"]
if item["key"] == "pchome_controlled_apply_drift_monitor"
)
assert summary["policy"] == "read_only_ai_automation_scheduled_health_summary"
assert summary["status"] == "ok"
assert summary["summary"]["total"] == 10
assert summary["summary"]["primary_human_gate_count"] == 0
assert summary["summary"]["writes_database_count"] == 0
assert pchome_family["status"] == "ok"
assert pchome_family["details"]["drift_count"] == 0
auto_policy_family = next(
item for item in summary["families"]
if item["key"] == "pchome_auto_policy_authorization_guard"
)
assert auto_policy_family["status"] == "ok"
assert auto_policy_family["details"]["lane_guard_pass_count"] == 11
assert auto_policy_family["details"]["primary_human_gate_count"] == 0
assert auto_policy_family["details"]["writes_database_count"] == 0
preflight_family = next(
item for item in summary["families"]
if item["key"] == "pchome_auto_policy_decision_preflight"
)
assert preflight_family["status"] == "ok"
assert preflight_family["details"]["decision_preflight_pass_count"] == 12
assert preflight_family["details"]["machine_fetch_evidence_ready_count"] == 1
assert preflight_family["details"]["payload_source"] == "local_contract_fixture"
assert preflight_family["details"]["outbound_network"] is False
assert preflight_family["details"]["business_data_source"] is False
assert preflight_family["details"]["primary_human_gate_count"] == 0
assert preflight_family["details"]["writes_database_count"] == 0
closeout_family = next(
item for item in summary["families"]
if item["key"] == "pchome_auto_policy_decision_closeout"
)
assert closeout_family["status"] == "ok"
assert closeout_family["details"]["decision_closeout_pass_count"] == 12
assert closeout_family["details"]["post_apply_verifier_required_count"] == 1
assert closeout_family["details"]["same_run_truth_required_count"] == 1
assert closeout_family["details"]["payload_source"] == "local_contract_fixture"
assert closeout_family["details"]["outbound_network"] is False
assert closeout_family["details"]["business_data_source"] is False
assert closeout_family["details"]["primary_human_gate_count"] == 0
assert closeout_family["details"]["writes_database_count"] == 0
surface_family = next(
item for item in summary["families"]
if item["key"] == "ai_surface_html_readback"
)
assert surface_family["status"] == "ok"
assert surface_family["details"]["checked_surface_count"] == 10
sitewide_family = next(
item for item in summary["families"]
if item["key"] == "sitewide_ui_ux_agent"
)
assert sitewide_family["status"] == "ok"
assert sitewide_family["details"]["issue_surface_count"] == 0
assert sitewide_family["details"]["repair_package_status"] == "no_op"
visual_family = next(
item for item in summary["families"]
if item["key"] == "sitewide_visual_qa"
)
assert visual_family["status"] == "ok"
assert visual_family["details"]["route_count"] == 12
assert visual_family["details"]["failed_count"] == 0
assert summary["scheduled_outputs"]["telegram_send_in_preview"] is False
assert summary["scheduled_outputs"]["writes_database"] is False
assert summary["automation_policy"]["primary_flow"] == "ai_controlled"
assert summary["safety"]["writes_database"] is False
def test_scheduled_automation_health_summary_can_use_current_smoke_without_recording_history(tmp_path, monkeypatch):
from services import ai_automation_smoke_service as smoke
history_path = tmp_path / "smoke_history.jsonl"
monkeypatch.setattr(smoke, "_HISTORY_PATH", str(history_path))
current_smoke = {
"generated_at": "2026-07-02T12:00:00",
"status": "critical",
"summary": {"ok": 12, "warning": 0, "critical": 1, "total": 13},
"checks": [
{
"name": "PChome 受控落地 drift monitor",
"status": "critical",
"summary": "PChome controlled apply 偵測到 1 筆 drift",
"details": {"drift_count": 1, "selector_count": 4, "readback_pass_count": 3},
},
_auto_policy_guard_history_check(),
_auto_policy_decision_preflight_history_check(),
_auto_policy_decision_closeout_history_check(),
{
"name": "AI surface HTML readback",
"status": "ok",
"summary": "AI surface HTML readback 通過 10/10無產品面 raw wording",
"details": {"checked_surface_count": 10, "pass_count": 10, "failed_count": 0},
},
{
"name": "Sitewide UI/UX Agent readback",
"status": "ok",
"summary": "Sitewide UI/UX Agent ok/no-op整站專業化 surface 無退化",
"details": {
"template_count": 55,
"compact_guardrail_count": 29,
"issue_surface_count": 0,
"repair_package_status": "no_op",
"repair_action_count": 0,
},
},
{
"name": "Sitewide visual QA readback",
"status": "ok",
"summary": "Sitewide visual QA ok12 routes x 3 viewports 無 overflow 退化",
"details": {
"route_count": 12,
"viewport_count": 3,
"failed_count": 0,
"overflow_issue_count": 0,
"visual_offender_count": 0,
"stale": False,
},
}
],
}
summary = smoke.build_scheduled_automation_health_summary(
current_smoke=current_smoke,
freshness_max_age_hours=9999,
)
assert summary["status"] == "critical"
assert "run_pchome_drift_verifier_and_recovery_package" in summary["next_machine_actions"]
assert summary["safety"]["include_current_smoke"] is True
assert not history_path.exists()
def test_scheduled_automation_health_summary_falls_back_to_visual_qa_artifact(tmp_path, monkeypatch):
from datetime import datetime
from services import ai_automation_smoke_service as smoke
from services import ai_surface_html_readback_service as surface_service
history_path = tmp_path / "smoke_history.jsonl"
history_path.write_text(
json.dumps({
"generated_at": datetime.now().isoformat(timespec="seconds"),
"status": "ok",
"summary": {"ok": 11, "warning": 0, "critical": 0, "total": 11},
"checks": [],
}, ensure_ascii=False) + "\n",
encoding="utf-8",
)
monkeypatch.setattr(smoke, "_HISTORY_PATH", str(history_path))
monkeypatch.setattr(
smoke,
"_pchome_auto_policy_authorization_guard_check",
lambda: _auto_policy_guard_history_check(),
)
monkeypatch.setattr(
smoke,
"_pchome_auto_policy_decision_preflight_check",
lambda: _auto_policy_decision_preflight_history_check(),
)
monkeypatch.setattr(
smoke,
"_pchome_auto_policy_decision_closeout_check",
lambda: _auto_policy_decision_closeout_history_check(),
)
monkeypatch.setattr(
surface_service,
"build_sitewide_visual_qa_readback",
lambda **_kwargs: {
"policy": "read_only_sitewide_visual_qa_readback_v1",
"status": "ok",
"artifact_path": "data/ai_automation/sitewide_visual_qa_latest.json",
"artifact_exists": True,
"artifact_generated_at": "2026-07-03T07:40:00",
"next_machine_action": "keep_sitewide_visual_qa_monitoring",
"summary": {
"result_count": 147,
"route_count": 49,
"viewport_count": 3,
"pass_count": 147,
"failed_count": 0,
"overflow_issue_count": 0,
"visual_offender_count": 0,
"stale": False,
},
},
)
summary = smoke.build_scheduled_automation_health_summary(
history_limit=5,
freshness_max_age_hours=9999,
)
visual_family = next(
item for item in summary["families"]
if item["key"] == "sitewide_visual_qa"
)
assert visual_family["status"] == "ok"
assert visual_family["details"]["route_count"] == 49
assert visual_family["details"]["failed_count"] == 0
def test_scheduled_automation_health_summary_route_returns_compact_payload(tmp_path, monkeypatch):
from datetime import datetime
from flask import Flask
from routes import system_public_routes as routes
from services import ai_automation_smoke_service as smoke
history_path = tmp_path / "smoke_history.jsonl"
history_path.write_text(
json.dumps({
"generated_at": datetime.now().isoformat(timespec="seconds"),
"status": "ok",
"summary": {"ok": 12, "warning": 0, "critical": 0, "total": 12},
"checks": [],
}, ensure_ascii=False) + "\n",
encoding="utf-8",
)
monkeypatch.setattr(smoke, "_HISTORY_PATH", str(history_path))
monkeypatch.setattr(
smoke,
"_pchome_auto_policy_authorization_guard_check",
lambda: _auto_policy_guard_history_check(),
)
monkeypatch.setattr(
smoke,
"_pchome_auto_policy_decision_preflight_check",
lambda: _auto_policy_decision_preflight_history_check(),
)
monkeypatch.setattr(
smoke,
"_pchome_auto_policy_decision_closeout_check",
lambda: _auto_policy_decision_closeout_history_check(),
)
app = Flask(__name__)
with app.test_request_context(
"/api/ai-automation/scheduled-health-summary?history_limit=5&freshness_max_age_hours=9999"
):
response = routes.ai_automation_scheduled_health_summary_api.__wrapped__()
payload = response.get_json()
assert payload["policy"] == "read_only_ai_automation_scheduled_health_summary"
assert payload["scheduled_outputs"]["health_summary_endpoint"] == (
"/api/ai-automation/scheduled-health-summary"
)
assert payload["safety"]["writes_database"] is False
def test_surface_html_readback_service_reports_compact_contracts():
from services.ai_surface_html_readback_service import build_ai_surface_html_readback
payload = build_ai_surface_html_readback()
assert payload["policy"] == "read_only_ai_surface_html_readback_v1"
assert payload["status"] == "ok"
assert payload["summary"]["checked_surface_count"] == 10
assert payload["summary"]["pass_count"] == 10
assert payload["summary"]["forbidden_leak_count"] == 0
assert payload["summary"]["primary_human_gate_count"] == 0
assert payload["summary"]["writes_database_count"] == 0
assert payload["safety"]["writes_database"] is False
def test_surface_html_readback_service_detects_regression():
from services.ai_surface_html_readback_service import build_ai_surface_html_readback
payload = build_ai_surface_html_readback(
rendered_html_by_route={
"/observability/overview": "<section>PPT_VISION_ENABLED=false</section>"
}
)
assert payload["status"] == "critical"
assert payload["summary"]["failed_count"] == 1
failed = payload["failed_surfaces"][0]
assert failed["route"] == "/observability/overview"
assert "compact-observability-workbench" in failed["missing_markers"]
assert "PPT_VISION_ENABLED=" in failed["forbidden_leaks"]
def test_surface_html_repair_package_builds_controlled_actions():
from services.ai_surface_html_readback_service import (
build_ai_surface_html_readback,
build_ai_surface_html_repair_package,
)
readback = build_ai_surface_html_readback(
rendered_html_by_route={
"/observability/overview": "<section>PPT_VISION_ENABLED=false</section>"
}
)
package = build_ai_surface_html_repair_package(source_readback=readback)
assert package["policy"] == "read_only_ai_surface_html_repair_package_v1"
assert package["status"] == "repair_ready"
assert package["summary"]["controlled_action_count"] == 2
assert package["summary"]["primary_human_gate_count"] == 0
assert package["summary"]["writes_database_count"] == 0
assert package["summary"]["executes_shell_count"] == 0
assert package["next_machine_action"] == "apply_ai_surface_html_contract_repair"
assert package["safety"]["read_only_package"] is True
actions = package["repair_items"][0]["controlled_actions"]
assert {item["action"] for item in actions} == {
"restore_density_guardrail_marker",
"remove_raw_engineering_copy_from_product_surface",
}
def test_sitewide_ui_ux_agent_inventory_scans_real_pages():
from services.ai_surface_html_readback_service import build_sitewide_ui_ux_agent_inventory
inventory = build_sitewide_ui_ux_agent_inventory()
assert inventory["policy"] == "read_only_sitewide_ui_ux_agent_inventory_v1"
assert inventory["summary"]["template_count"] >= 50
assert inventory["summary"]["high_priority_template_count"] >= 20
assert inventory["summary"]["compact_guardrail_count"] >= 29
assert inventory["summary"]["issue_surface_count"] == 0
assert inventory["summary"]["primary_human_gate_count"] == 0
assert inventory["summary"]["writes_database_count"] == 0
assert inventory["safety"]["writes_database"] is False
templates = {item["template"] for item in inventory["surfaces"]}
issue_templates = {item["template"] for item in inventory["issue_surfaces"]}
assert "templates/base.html" not in templates
assert not any("/components/" in item for item in templates)
assert not any("/." in item or item.startswith("templates/.") for item in templates)
assert "templates/ai_history.html" not in issue_templates
assert "templates/ai_recommend.html" not in issue_templates
assert "templates/admin/ppt_audit_preview.html" not in issue_templates
assert "templates/cicd_dashboard.html" not in issue_templates
assert "templates/daily_sales.html" not in issue_templates
assert "templates/dashboard.html" not in issue_templates
assert "templates/edm_dashboard.html" not in issue_templates
assert "templates/growth_analysis.html" not in issue_templates
assert "templates/pchome_crawler.html" not in issue_templates
assert "templates/price_comparison.html" not in issue_templates
assert "templates/sales_analysis.html" not in issue_templates
assert "templates/vendor_stockout_history_v2.html" not in issue_templates
assert "templates/vendor_stockout_import_v2.html" not in issue_templates
assert "templates/vendor_stockout_index_v2.html" not in issue_templates
assert "templates/vendor_stockout_list_v2.html" not in issue_templates
assert "templates/vendor_stockout_send_email_v2.html" not in issue_templates
assert "templates/vendor_stockout_vendor_management_v2.html" not in issue_templates
def test_sitewide_ui_ux_repair_package_prioritizes_controlled_template_repairs():
from services.ai_surface_html_readback_service import (
build_sitewide_ui_ux_agent_inventory,
build_sitewide_ui_ux_repair_package,
)
inventory = build_sitewide_ui_ux_agent_inventory()
package = build_sitewide_ui_ux_repair_package(source_inventory=inventory, limit=3)
assert package["policy"] == "read_only_sitewide_ui_ux_repair_package_v1"
assert package["summary"]["selected_surface_count"] <= 3
assert package["summary"]["primary_human_gate_count"] == 0
assert package["summary"]["writes_database_count"] == 0
assert package["summary"]["executes_shell_count"] == 0
assert package["safety"]["read_only_package"] is True
assert "templates/**/*.html" in package["controlled_apply_contract"]["allowed_target_globs"]
if package["repair_items"]:
assert package["next_machine_action"] == "apply_sitewide_ui_ux_controlled_repairs"
assert package["repair_items"][0]["controlled_actions"]
def test_sitewide_ui_ux_agent_reports_non_utf8_templates(tmp_path):
from services.ai_surface_html_readback_service import (
build_sitewide_ui_ux_agent_inventory,
build_sitewide_ui_ux_repair_package,
)
template_dir = tmp_path / "templates"
template_dir.mkdir()
routes_dir = tmp_path / "routes"
routes_dir.mkdir()
(template_dir / "legacy_dashboard.html").write_bytes(b"\xa3 legacy dashboard")
(template_dir / "._legacy_dashboard.html").write_bytes(b"\xa3 apple metadata")
(routes_dir / "legacy_routes.py").write_bytes(b"\xa3 legacy route source")
inventory = build_sitewide_ui_ux_agent_inventory(root=tmp_path)
issue = inventory["issue_surfaces"][0]
package = build_sitewide_ui_ux_repair_package(source_inventory=inventory)
assert inventory["status"] == "warning"
assert inventory["summary"]["template_count"] == 1
assert issue["status"] == "critical"
assert issue["findings"][0]["type"] == "template_encoding_error"
assert package["repair_items"][0]["controlled_actions"][0]["action"] == (
"normalize_template_encoding_utf8"
)
assert package["safety"]["writes_database"] is False
def test_sitewide_visual_qa_readback_consumes_artifact_payload():
from datetime import datetime
from services.ai_surface_html_readback_service import build_sitewide_visual_qa_readback
payload = build_sitewide_visual_qa_readback(
artifact_payload={
"generated_at": datetime.now().isoformat(timespec="seconds"),
"results": [
{"route": "/ai_intelligence", "viewport": "desktop-1440", "passed": True},
{"route": "/ai_intelligence", "viewport": "mobile-390", "passed": True},
{"route": "/observability/overview", "viewport": "mobile-390", "passed": True},
],
}
)
assert payload["policy"] == "read_only_sitewide_visual_qa_readback_v1"
assert payload["status"] == "ok"
assert payload["summary"]["result_count"] == 3
assert payload["summary"]["route_count"] == 2
assert payload["summary"]["viewport_count"] == 2
assert payload["summary"]["failed_count"] == 0
assert payload["summary"]["primary_human_gate_count"] == 0
assert payload["summary"]["writes_database_count"] == 0
assert payload["safety"]["requires_browser_for_readback"] is False
assert payload["safety"]["requires_browser_for_artifact_generation"] is True
def test_sitewide_visual_qa_readback_reports_visual_failures():
from datetime import datetime
from services.ai_surface_html_readback_service import build_sitewide_visual_qa_readback
payload = build_sitewide_visual_qa_readback(
artifact_payload={
"generated_at": datetime.now().isoformat(timespec="seconds"),
"results": [
{
"route": "/price_comparison",
"viewport": "mobile-390",
"passed": False,
"error": "body horizontal overflow 24px",
}
],
}
)
assert payload["status"] == "critical"
assert payload["summary"]["failed_count"] == 1
assert payload["summary"]["overflow_issue_count"] == 1
assert payload["next_machine_action"] == "run_sitewide_visual_qa_and_publish_artifact"
assert payload["failed_results"][0]["route"] == "/price_comparison"
def test_surface_html_readback_check_is_part_of_ai_smoke(monkeypatch):
from services import ai_automation_smoke_service as smoke
monkeypatch.setattr(smoke, "_event_router_check", lambda: smoke._check("event", "ok", "ok"))
monkeypatch.setattr(smoke, "_gemini_egress_check", lambda: smoke._check("gemini", "ok", "ok"))
monkeypatch.setattr(smoke, "_autoheal_check", lambda: smoke._check("autoheal", "ok", "ok"))
monkeypatch.setattr(smoke, "_nemotron_check", lambda: smoke._check("nemotron", "ok", "ok"))
monkeypatch.setattr(smoke, "_embedding_queue_check", lambda: smoke._check("embedding", "ok", "ok"))
monkeypatch.setattr(smoke, "_elephant_hitl_check", lambda: smoke._check("elephant", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_controlled_apply_drift_monitor_check", lambda: smoke._check("pchome", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_auto_policy_authorization_guard_check", lambda: smoke._check("auto-policy", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_auto_policy_decision_preflight_check", lambda: smoke._check("auto-policy preflight", "ok", "ok"))
monkeypatch.setattr(smoke, "_pchome_auto_policy_decision_closeout_check", lambda: smoke._check("auto-policy closeout", "ok", "ok"))
monkeypatch.setattr(smoke, "_sitewide_visual_qa_check", lambda: smoke._check(
"Sitewide visual QA readback",
"ok",
"visual ok",
{"route_count": 12, "viewport_count": 3, "failed_count": 0},
))
result = smoke.collect_ai_automation_smoke(record_history=False)
surface_check = next(
item for item in result["checks"]
if item["name"] == "AI surface HTML readback"
)
sitewide_check = next(
item for item in result["checks"]
if item["name"] == "Sitewide UI/UX Agent readback"
)
visual_check = next(
item for item in result["checks"]
if item["name"] == "Sitewide visual QA readback"
)
assert result["summary"]["total"] == 13
assert surface_check["status"] == "ok"
assert surface_check["details"]["checked_surface_count"] == 10
assert sitewide_check["status"] == "ok"
assert sitewide_check["details"]["issue_surface_count"] == 0
assert sitewide_check["details"]["repair_package_status"] == "no_op"
assert visual_check["status"] == "ok"
assert visual_check["details"]["route_count"] == 12
def test_surface_html_readback_route_returns_payload(monkeypatch):
from flask import Flask
from routes import system_public_routes as routes
app = Flask(__name__)
with app.test_request_context("/api/ai-automation/surface-html-readback"):
response = routes.ai_automation_surface_html_readback_api.__wrapped__()
payload = response.get_json()
assert payload["policy"] == "read_only_ai_surface_html_readback_v1"
assert payload["summary"]["checked_surface_count"] == 10
assert payload["safety"]["writes_database"] is False
def test_surface_and_sitewide_ui_ux_agent_routes_return_payloads():
from flask import Flask
from routes import system_public_routes as routes
app = Flask(__name__)
route_cases = [
(
"/api/ai-automation/surface-html-repair-package",
routes.ai_automation_surface_html_repair_package_api.__wrapped__,
"read_only_ai_surface_html_repair_package_v1",
),
(
"/api/ai-automation/sitewide-ui-ux-agent",
routes.ai_automation_sitewide_ui_ux_agent_api.__wrapped__,
"read_only_sitewide_ui_ux_agent_inventory_v1",
),
(
"/api/ai-automation/sitewide-ui-ux-repair-package?limit=3",
routes.ai_automation_sitewide_ui_ux_repair_package_api.__wrapped__,
"read_only_sitewide_ui_ux_repair_package_v1",
),
(
"/api/ai-automation/sitewide-visual-qa-readback?max_age_hours=9999",
routes.ai_automation_sitewide_visual_qa_readback_api.__wrapped__,
"read_only_sitewide_visual_qa_readback_v1",
),
]
for path, view_func, policy in route_cases:
with app.test_request_context(path):
response = view_func()
payload = response.get_json()
assert payload["policy"] == policy
assert payload["safety"].get("writes_database") is False
def test_gemini_egress_check_ok_when_no_calls(monkeypatch):
from services import ai_automation_smoke_service as smoke
class FakeResult:
def fetchone(self):
return {"calls": 0, "tokens": 0, "cost_usd": 0, "last_called": None}
def fetchall(self):
return []
class FakeSession:
closed = False
def execute(self, *_args, **_kwargs):
return FakeResult()
def close(self):
self.closed = True
fake_session = FakeSession()
monkeypatch.setattr(smoke, "get_session", lambda: fake_session)
monkeypatch.delenv("GEMINI_API_HARD_DISABLED", raising=False)
result = smoke._gemini_egress_check()
assert result["status"] == "ok"
assert result["details"]["calls"] == 0
assert result["details"]["hard_disabled"] is True
assert fake_session.closed is True
def test_gemini_egress_check_critical_when_hard_disabled_has_calls(monkeypatch):
from services import ai_automation_smoke_service as smoke
class FakeSummaryResult:
def fetchone(self):
return {
"calls": 2,
"tokens": 1234,
"cost_usd": 0.012345,
"last_called": "2026-05-21 07:00:00+00",
}
class FakeTopResult:
def fetchall(self):
return [{
"caller": "code_review_openclaw_gemini",
"model": "gemini-2.5-flash",
"calls": 2,
"tokens": 1234,
"cost_usd": 0.012345,
"last_called": "2026-05-21 07:00:00+00",
}]
class FakeSession:
def __init__(self):
self.calls = 0
def execute(self, *_args, **_kwargs):
self.calls += 1
return FakeSummaryResult() if self.calls == 1 else FakeTopResult()
def close(self):
pass
monkeypatch.setattr(smoke, "get_session", lambda: FakeSession())
monkeypatch.delenv("GEMINI_API_HARD_DISABLED", raising=False)
result = smoke._gemini_egress_check()
assert result["status"] == "critical"
assert "Hard-disabled" in result["summary"]
assert result["details"]["calls"] == 2
assert result["details"]["top_callers"][0]["caller"] == "code_review_openclaw_gemini"
def test_nemotron_smoke_detects_current_dispatcher_fallback(monkeypatch):
from services import ai_automation_smoke_service as smoke
import services.nemoton_dispatcher_service as nemotron
monkeypatch.delattr(nemotron, "NemotronDispatcherService", raising=False)
monkeypatch.setattr(nemotron, "NIM_API_KEY", "")
result = smoke._nemotron_check()
assert result["status"] == "warning"
assert result["details"]["fallback_ready"] is True
assert result["details"]["dispatcher_class"] == "NemotronDispatcher"
def test_build_smoke_daily_summary_message_escapes_history(tmp_path, monkeypatch):
from services import ai_automation_smoke_service as smoke
history_path = tmp_path / "smoke_history.jsonl"
history_path.write_text(
'{"generated_at":"2026-04-29T01:00:00","status":"warning",'
'"summary":{"ok":0,"warning":1,"critical":0,"total":1},'
'"checks":[{"name":"<script>","status":"warning","summary":"bad"}]}\n',
encoding="utf-8",
)
monkeypatch.setattr(smoke, "_HISTORY_PATH", str(history_path))
message = smoke.build_smoke_daily_summary_message()
assert "AI 自動化 Smoke 每日摘要" in message
assert "WARNING" in message
assert "最近異常檢查" in message
assert "<script>" not in message
def test_send_smoke_daily_summary_uses_telegram_result(monkeypatch):
from services import ai_automation_smoke_service as smoke
monkeypatch.setattr(smoke, "build_smoke_daily_summary_message", lambda: "hello")
monkeypatch.setattr(
"services.telegram_templates.send_telegram_with_result",
lambda message, chat_ids=None: {
"ok": True,
"sent": 1,
"failed": 0,
"chat_ids": chat_ids or [123],
"errors": [],
},
)
result = smoke.send_smoke_daily_summary(chat_ids=[999])
assert result["status"] == "sent"
assert result["telegram"]["chat_ids"] == [999]