feat(governance): automate identity and comparison coverage

This commit is contained in:
ogt
2026-07-14 16:15:41 +08:00
parent 2dcc2122c4
commit 4bfb9dbefd
31 changed files with 2763 additions and 578 deletions

View File

@@ -1258,8 +1258,8 @@ def test_anon_get_redirects_to_login(anon_client):
'/observability/api/health_indicator',
]:
r = anon_client.get(path)
# 308permanent redirect for trailing slash或 302login redirect皆視為阻擋
assert r.status_code in (302, 308), f'{path} 未強制 login (got {r.status_code})'
expected = (401,) if '/api/' in path else (302, 308)
assert r.status_code in expected, f'{path} 未強制 login (got {r.status_code})'
def test_anon_post_blocked(anon_client):

View File

@@ -84,6 +84,26 @@ def test_ai_intelligence_runtime_task_copy_stays_short():
assert short_task in command_renderer
def test_ai_intelligence_comparison_coverage_has_explicit_scope_and_partition():
template = _template()
renderer = template.split("function renderGrowthCommandCenter", 1)[1].split(
"function renderGrowthCommandTopDecliners",
1,
)[0]
assert "同款比價證據 · TOP 50" in template
assert "正式資料平台" in template
assert "比價覆蓋 · TOP 50" in template
assert "具正式比價證據" in template
assert "候選 / 來源驗證" in template
assert "尚未找到同款" in template
assert "比價可用率" not in template
assert "needsMapping + reviewCandidateCount" not in renderer
assert "commandComparisonReady" in renderer
assert "commandCandidateValidation" in renderer
assert "commandUnmatched" in renderer
def test_ai_intelligence_detail_meta_and_single_product_avoid_long_reasons():
template = _template()
detail_config = template.split("function growthDetailConfig", 1)[1].split(

View File

@@ -0,0 +1,537 @@
from datetime import datetime, timedelta
from flask import Flask
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from werkzeug.security import generate_password_hash
import auth
import services.permission_service as permission_service_module
from database.permission_models import UserPermission
from database.user_models import LoginHistory, User
from services.auth_identity_service import (
build_auth_identity_runtime_readback,
required_roles_for_request,
resolve_auth_identity_mode,
resolve_client_ip,
)
from services.user_service import UserService
from services.permission_service import PermissionService
@pytest.fixture
def identity_store():
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
User.__table__.create(engine)
LoginHistory.__table__.create(engine)
UserPermission.__table__.create(engine)
return sessionmaker(bind=engine)
@pytest.fixture
def identity_user(identity_store):
db_session = identity_store()
now = datetime.now()
user = User(
username="operator-admin",
password_hash=generate_password_hash("DatabasePass9!", method="pbkdf2:sha256"),
role="admin",
display_name="Operator Admin",
is_active=True,
password_changed_at=now,
created_at=now,
updated_at=now,
)
db_session.add(user)
db_session.commit()
user_id = user.id
db_session.close()
return user_id
@pytest.fixture
def identity_app(monkeypatch, identity_store, identity_user):
monkeypatch.setenv("MOMO_AUTH_IDENTITY_MODE", "hybrid")
monkeypatch.setenv("MOMO_TRUSTED_PROXY_CIDRS", "127.0.0.1/32,::1/128")
monkeypatch.setattr(auth, "LOGIN_PASSWORD", "LegacyPass9!")
monkeypatch.setattr(auth, "get_identity_session", identity_store)
app = Flask(__name__, template_folder=str(auth.os.path.join(auth.os.path.dirname(__file__), "..", "templates")))
app.config.update(
TESTING=True,
SECRET_KEY="identity-test-secret",
SESSION_COOKIE_SECURE=False,
)
app.jinja_env.globals["csrf_token"] = lambda: "test-csrf"
app.add_url_rule(
"/",
endpoint="dashboard.index",
view_func=lambda: "dashboard",
)
@app.route("/protected")
@auth.login_required
def protected():
return "protected"
auth.init_auth_routes(app)
return app
def test_proxy_headers_are_ignored_from_untrusted_peer():
assert resolve_client_ip(
"203.0.113.9",
"198.51.100.7",
trusted_proxy_cidrs="127.0.0.1/32",
) == "203.0.113.9"
assert resolve_client_ip(
"127.0.0.1",
"198.51.100.7, 127.0.0.1",
trusted_proxy_cidrs="127.0.0.1/32",
) == "198.51.100.7"
def test_auth_mode_and_role_matrix_fail_closed():
assert resolve_auth_identity_mode("auto") == "auto"
with pytest.raises(ValueError):
resolve_auth_identity_mode("invalid")
assert required_roles_for_request("GET", "vendor.list", "vendor") == (
"admin",
"manager",
"user",
)
assert required_roles_for_request("POST", "vendor.update", "vendor") == (
"admin",
"manager",
)
assert required_roles_for_request("GET", "cicd.status", "cicd") == ("admin",)
def test_lockout_is_durable_across_database_sessions(identity_store):
for _ in range(5):
db_session = identity_store()
assert UserService(db_session).record_login(
None,
"operator-admin",
"198.51.100.20",
"pytest",
LoginHistory.STATUS_FAILED,
"invalid_credentials",
) is not None
db_session.close()
db_session = identity_store()
service = UserService(db_session)
locked, remaining, count = service.get_lockout_status(
"operator-admin",
"198.51.100.20",
max_attempts=5,
reset_seconds=1800,
lockout_seconds=300,
)
assert locked is True
assert remaining > 0
assert count == 5
service.record_login(
None,
"operator-admin",
"198.51.100.20",
"pytest",
LoginHistory.STATUS_SUCCESS,
"auth_source=database",
)
locked, remaining, count = service.get_lockout_status(
"operator-admin",
"198.51.100.20",
)
assert (locked, remaining, count) == (False, 0, 0)
db_session.close()
def test_database_identity_login_writes_audit_and_versioned_session(
identity_app,
identity_store,
identity_user,
):
client = identity_app.test_client()
response = client.post(
"/login",
data={"username": "operator-admin", "password": "DatabasePass9!"},
)
assert response.status_code == 302
assert response.headers["Location"].endswith("/")
with client.session_transaction() as current_session:
assert current_session["user_id"] == identity_user
assert current_session["role"] == "admin"
assert current_session["auth_source"] == "database"
assert current_session["identity_version"]
db_session = identity_store()
event = db_session.query(LoginHistory).order_by(LoginHistory.id.desc()).first()
assert event.status == LoginHistory.STATUS_SUCCESS
assert event.failure_reason == "auth_source=database"
db_session.close()
def test_identity_change_revokes_existing_session(
identity_app,
identity_store,
identity_user,
):
client = identity_app.test_client()
assert client.post(
"/login",
data={"username": "operator-admin", "password": "DatabasePass9!"},
).status_code == 302
db_session = identity_store()
user = db_session.query(User).filter(User.id == identity_user).one()
user.updated_at = datetime.now() + timedelta(seconds=1)
db_session.commit()
db_session.close()
response = client.get("/protected")
assert response.status_code == 302
assert response.headers["Location"].endswith("/login")
with client.session_transaction() as current_session:
assert "logged_in" not in current_session
db_session = identity_store()
statuses = [row.status for row in db_session.query(LoginHistory).all()]
assert LoginHistory.STATUS_REVOKED in statuses
db_session.close()
def test_legacy_hybrid_login_audits_real_peer_not_spoofed_header(
identity_app,
identity_store,
):
client = identity_app.test_client()
response = client.post(
"/login",
data={"username": "", "password": "LegacyPass9!"},
headers={"X-Forwarded-For": "198.51.100.88"},
environ_base={"REMOTE_ADDR": "203.0.113.44"},
)
assert response.status_code == 302
with client.session_transaction() as current_session:
assert current_session["auth_source"] == "legacy_shared_password"
assert current_session["client_ip"] == "203.0.113.44"
db_session = identity_store()
event = db_session.query(LoginHistory).order_by(LoginHistory.id.desc()).first()
assert event.ip_address == "203.0.113.44"
db_session.close()
def test_database_mode_refuses_legacy_authority(
monkeypatch,
identity_app,
):
monkeypatch.setenv("MOMO_AUTH_IDENTITY_MODE", "database")
client = identity_app.test_client()
response = client.post(
"/login",
data={"username": "", "password": "LegacyPass9!"},
)
assert response.status_code == 401
with client.session_transaction() as current_session:
assert "logged_in" not in current_session
def test_auto_mode_retires_legacy_session_after_durable_admin_canary(
monkeypatch,
identity_app,
identity_store,
identity_user,
):
monkeypatch.setenv("MOMO_AUTH_IDENTITY_MODE", "auto")
client = identity_app.test_client()
assert client.post(
"/login",
data={"username": "", "password": "LegacyPass9!"},
).status_code == 302
db_session = identity_store()
service = UserService(db_session)
for _ in range(2):
assert service.record_login(
identity_user,
"operator-admin",
"127.0.0.1",
"pytest",
LoginHistory.STATUS_SUCCESS,
"auth_source=database",
) is not None
assert service.get_auth_auto_cutover_state()["ready"] is True
db_session.close()
response = client.get("/protected")
assert response.status_code == 302
assert response.headers["Location"].endswith("/login")
with client.session_transaction() as current_session:
assert "logged_in" not in current_session
def test_unversioned_cookie_is_rejected_outside_test_mode(monkeypatch, identity_store):
monkeypatch.setenv("MOMO_AUTH_IDENTITY_MODE", "hybrid")
monkeypatch.setattr(auth, "get_identity_session", identity_store)
app = Flask(__name__)
app.config.update(SECRET_KEY="production-shape-secret", TESTING=False)
@app.route("/login", endpoint="login")
def login_stub():
return "login"
@app.route("/protected-production-shape")
@auth.login_required
def protected_production_shape():
return "protected"
client = app.test_client()
with client.session_transaction() as current_session:
current_session["logged_in"] = True
response = client.get("/protected-production-shape")
assert response.status_code == 302
assert response.headers["Location"].endswith("/login")
with client.session_transaction() as current_session:
assert "logged_in" not in current_session
def test_runtime_readback_separates_canary_from_closed(identity_store, identity_user):
db_session = identity_store()
report = build_auth_identity_runtime_readback(db_session, mode="hybrid")
assert report["status"] == "canary_ready"
assert report["canary_ready"] is True
assert report["runtime_closed"] is False
assert report["inventory"]["active_admin_count"] == 1
assert report["checks"]["legacy_shared_password_retired"] is False
closed = build_auth_identity_runtime_readback(db_session, mode="database")
assert closed["status"] == "completed"
assert closed["runtime_closed"] is True
assert closed["checks"]["legacy_shared_password_retired"] is True
db_session.close()
def test_auto_runtime_readback_closes_after_admin_success_threshold(
identity_store,
identity_user,
):
db_session = identity_store()
initial = build_auth_identity_runtime_readback(db_session, mode="auto")
assert initial["configured_auth_identity_mode"] == "auto"
assert initial["auth_identity_mode"] == "hybrid"
assert initial["runtime_closed"] is False
service = UserService(db_session)
for _ in range(2):
service.record_login(
identity_user,
"operator-admin",
"127.0.0.1",
"pytest",
LoginHistory.STATUS_SUCCESS,
"auth_source=database",
)
promoted = build_auth_identity_runtime_readback(db_session, mode="auto")
assert promoted["configured_auth_identity_mode"] == "auto"
assert promoted["auth_identity_mode"] == "database"
assert promoted["auto_cutover"]["manual_review_required"] is False
assert promoted["auto_cutover"]["database_admin_successes"] == 2
assert promoted["runtime_closed"] is True
db_session.close()
def test_identity_mutations_write_no_secret_audits(
identity_store,
identity_user,
):
audit_context = {
"actor_id": identity_user,
"ip_address": "198.51.100.40",
"user_agent": "pytest-identity-audit",
}
db_session = identity_store()
service = UserService(db_session)
user, error = service.create_user(
"audit-target",
"CreatePass9!",
role="admin",
created_by=identity_user,
audit_context=audit_context,
)
assert error is None
success, error = service.update_user(
user.id,
audit_context=audit_context,
role="user",
display_name="Audit Target",
)
assert (success, error) == (True, None)
success, error = service.change_password(
user.id,
"CreatePass9!",
"ChangedPass8!",
audit_context=audit_context,
)
assert (success, error) == (True, None)
success, error = service.reset_password(
user.id,
"ResetPass7!",
admin_id=identity_user,
audit_context=audit_context,
)
assert (success, error) == (True, None)
success, error = service.delete_user(user.id, audit_context=audit_context)
assert (success, error) == (True, None)
events = db_session.query(LoginHistory).filter(
LoginHistory.user_id == user.id,
).order_by(LoginHistory.id).all()
assert [event.status for event in events] == [
LoginHistory.STATUS_USER_CREATED,
LoginHistory.STATUS_IDENTITY_CHANGED,
LoginHistory.STATUS_PASSWORD_CHANGED,
LoginHistory.STATUS_PASSWORD_CHANGED,
LoginHistory.STATUS_USER_DISABLED,
]
serialized = " ".join(
f"{event.failure_reason or ''} {event.user_agent or ''}"
for event in events
)
assert "CreatePass9!" not in serialized
assert "ChangedPass8!" not in serialized
assert "ResetPass7!" not in serialized
assert all(event.ip_address == "198.51.100.40" for event in events)
assert service.get_identity_inventory()["identity_mutation_events"] == 5
db_session.close()
def test_identity_mutation_and_audit_roll_back_together(
monkeypatch,
identity_store,
identity_user,
):
db_session = identity_store()
def fail_commit():
raise RuntimeError("forced commit failure")
monkeypatch.setattr(db_session, "commit", fail_commit)
user, error = UserService(db_session).create_user(
"rollback-target",
"RollbackPass9!",
role="admin",
created_by=identity_user,
audit_context={"actor_id": identity_user},
)
assert user is None
assert "forced commit failure" in error
db_session.close()
verification_session = identity_store()
assert verification_session.query(User).filter_by(username="rollback-target").count() == 0
assert verification_session.query(LoginHistory).filter_by(
username_attempted="rollback-target",
).count() == 0
verification_session.close()
def test_permission_mutation_and_audit_share_one_transaction(
monkeypatch,
identity_store,
identity_user,
):
seed_session = identity_store()
target = User(
username="permission-target",
password_hash=generate_password_hash("PermissionPass9!", method="pbkdf2:sha256"),
role="user",
display_name="Permission Target",
is_active=True,
password_changed_at=datetime.now(),
created_at=datetime.now(),
)
seed_session.add(target)
seed_session.commit()
target_id = target.id
seed_session.close()
monkeypatch.setattr(permission_service_module, "get_session", identity_store)
success, message = PermissionService.set_user_permissions(
target_id,
["dashboard.view", "dashboard.view"],
granted_by=identity_user,
audit_context={
"actor_id": identity_user,
"ip_address": "198.51.100.41",
"user_agent": "pytest-permission-audit",
},
)
assert success is True
assert "1 項權限" in message
verification_session = identity_store()
assert verification_session.query(UserPermission).filter_by(user_id=target_id).count() == 1
event = verification_session.query(LoginHistory).filter_by(
user_id=target_id,
status=LoginHistory.STATUS_PERMISSION_CHANGED,
).one()
assert event.failure_reason == f"actor_id={identity_user};change=replace;count=1"
assert event.ip_address == "198.51.100.41"
assert event.user_agent == "pytest-permission-audit"
verification_session.close()
def test_permission_mutation_and_audit_roll_back_on_commit_failure(
monkeypatch,
identity_store,
identity_user,
):
seed_session = identity_store()
target = User(
username="permission-rollback",
password_hash=generate_password_hash("PermissionPass9!", method="pbkdf2:sha256"),
role="user",
display_name="Permission Rollback",
is_active=True,
password_changed_at=datetime.now(),
created_at=datetime.now(),
)
seed_session.add(target)
seed_session.commit()
target_id = target.id
seed_session.close()
failing_session = identity_store()
def fail_commit():
raise RuntimeError("forced permission commit failure")
monkeypatch.setattr(failing_session, "commit", fail_commit)
monkeypatch.setattr(permission_service_module, "get_session", lambda: failing_session)
success, message = PermissionService.set_user_permissions(
target_id,
["dashboard.view"],
granted_by=identity_user,
audit_context={"actor_id": identity_user},
)
assert success is False
assert "forced permission commit failure" in message
verification_session = identity_store()
assert verification_session.query(UserPermission).filter_by(user_id=target_id).count() == 0
assert verification_session.query(LoginHistory).filter_by(
user_id=target_id,
status=LoginHistory.STATUS_PERMISSION_CHANGED,
).count() == 0
verification_session.close()

View File

@@ -384,6 +384,9 @@ def test_sales_analysis_preview_context_cache_avoids_reloading_options(tmp_path,
for _ in range(2):
with app.test_request_context("/sales_analysis"):
session["logged_in"] = True
session["role"] = "admin"
session["username"] = "legacy-admin"
session["auth_source"] = "legacy_shared_password"
response = sales_routes.sales_analysis()
assert response["no_filter"] is True
assert response["all_categories"] == ["美妝"]

View File

@@ -12,6 +12,9 @@ def _client():
client = app.test_client()
with client.session_transaction() as session:
session["logged_in"] = True
session["role"] = "admin"
session["username"] = "legacy-admin"
session["auth_source"] = "legacy_shared_password"
return client

View File

@@ -98,6 +98,12 @@ from services.market_intel.schema_db_probe import build_schema_db_probe_plan
TEST_APPROVAL_TOKEN = "test-market-intel-approval-token"
def _build_test_app():
test_app = Flask(__name__)
test_app.config["TESTING"] = True
return test_app
def _market_intel_sample_result(batch_id="sample-batch-receipt"):
return {
"batch_id": batch_id,
@@ -2095,7 +2101,7 @@ def test_mcp_tool_contract_detects_missing_router_whitelist():
def test_mcp_tool_contract_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -2179,7 +2185,7 @@ def test_mcp_activation_runbook_can_be_ready_with_mocked_gates():
def test_mcp_activation_runbook_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -2262,7 +2268,7 @@ def test_mcp_fetch_gate_can_open_with_mocked_ready_state():
def test_mcp_fetch_gate_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -2350,7 +2356,7 @@ def test_mcp_completion_audit_detects_missing_internal_contract():
def test_mcp_completion_audit_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -2448,7 +2454,7 @@ def test_mcp_activation_evidence_blocks_secret_literals_and_write_flags():
def test_mcp_activation_evidence_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -2559,7 +2565,7 @@ def test_mcp_runtime_smoke_receipt_blocks_incomplete_or_write_receipt():
def test_mcp_runtime_smoke_receipt_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -2665,7 +2671,7 @@ def test_mcp_runtime_promotion_blocks_partial_or_unsafe_package():
def test_mcp_runtime_promotion_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -2773,7 +2779,7 @@ def test_mcp_manual_fetch_handoff_blocks_missing_acknowledgement():
def test_mcp_manual_fetch_handoff_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -2901,7 +2907,7 @@ def test_mcp_fetch_target_review_blocks_unknown_or_unsafe_target():
def test_mcp_fetch_target_review_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -3030,7 +3036,7 @@ def test_mcp_fetch_run_package_blocks_unsafe_controls():
def test_mcp_fetch_run_package_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -3169,7 +3175,7 @@ def test_mcp_fetch_run_readiness_blocks_unsafe_operator_payload():
def test_mcp_fetch_run_readiness_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -3313,7 +3319,7 @@ def test_mcp_fetch_run_receipt_blocks_unsafe_receipt_payload():
def test_mcp_fetch_run_receipt_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -3467,7 +3473,7 @@ def test_mcp_fetch_result_parser_review_blocks_unsafe_parser_payload():
def test_mcp_fetch_result_parser_review_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -3626,7 +3632,7 @@ def test_mcp_fetch_candidate_handoff_review_blocks_unsafe_handoff():
def test_mcp_fetch_candidate_handoff_review_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -3785,7 +3791,7 @@ def test_mcp_fetch_candidate_queue_review_blocks_unsafe_review():
def test_mcp_fetch_candidate_queue_review_route_get_and_post_are_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -3957,7 +3963,7 @@ def test_mcp_fetch_candidate_queue_writer_preflight_blocks_unsafe_preflight():
def test_mcp_fetch_candidate_queue_writer_preflight_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -4131,7 +4137,7 @@ def test_mcp_fetch_candidate_queue_writer_cli_review_blocks_unsafe_review():
def test_mcp_fetch_candidate_queue_writer_cli_review_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -4343,7 +4349,7 @@ def test_mcp_fetch_candidate_queue_writer_run_package_review_blocks_unsafe_revie
def test_mcp_fetch_candidate_queue_writer_run_package_review_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -4551,7 +4557,7 @@ def test_mcp_fetch_candidate_queue_writer_run_readiness_blocks_unsafe_review():
def test_mcp_fetch_candidate_queue_writer_run_readiness_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -4774,7 +4780,7 @@ def test_mcp_fetch_candidate_queue_writer_run_receipt_review_blocks_unsafe_revie
def test_mcp_fetch_candidate_queue_writer_run_receipt_review_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -5014,7 +5020,7 @@ def test_mcp_fetch_candidate_queue_writer_run_closeout_review_blocks_unsafe_revi
def test_mcp_fetch_candidate_queue_writer_run_closeout_review_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -5275,7 +5281,7 @@ def test_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_blocks_
def test_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -5531,7 +5537,7 @@ def test_mcp_fetch_candidate_queue_writer_review_handoff_blocks_unsafe_review():
def test_mcp_fetch_candidate_queue_writer_review_handoff_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -5758,7 +5764,7 @@ def test_mcp_fetch_candidate_queue_writer_review_inventory_blocks_unsafe_invento
def test_mcp_fetch_candidate_queue_writer_review_inventory_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -5945,7 +5951,7 @@ def test_mcp_fetch_candidate_queue_writer_review_inventory_blocks_bad_inventory_
def test_mcp_fetch_candidate_queue_writer_review_inventory_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -6198,7 +6204,7 @@ def test_mcp_fetch_candidate_queue_writer_review_decision_blocks_unsafe_decision
def test_mcp_fetch_candidate_queue_writer_review_decision_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -6459,7 +6465,7 @@ def test_mcp_fetch_candidate_queue_writer_review_decision_approval_blocks_unsafe
def test_mcp_fetch_candidate_queue_writer_review_decision_approval_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -6766,7 +6772,7 @@ def test_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_prefli
def test_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -6867,7 +6873,7 @@ def test_manual_sample_plan_preview_blocks_fetch_and_write():
def test_manual_sample_plan_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -6920,7 +6926,7 @@ def test_manual_sample_acceptance_preview_blocks_candidate_import():
def test_manual_sample_acceptance_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -7043,7 +7049,7 @@ def test_manual_sample_review_evaluator_rejects_bad_result():
def test_manual_sample_review_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -7117,7 +7123,7 @@ def test_manual_sample_review_evaluation_preview_accepts_payload_without_persist
def test_manual_sample_review_evaluate_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -7172,7 +7178,7 @@ def test_manual_sample_review_evaluate_route_is_post_only_and_no_write():
def test_manual_sample_review_evaluate_rejects_invalid_json_without_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -7261,7 +7267,7 @@ def test_manual_sample_candidate_handoff_preview_creates_candidates_without_pers
def test_manual_sample_candidate_handoff_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -7394,7 +7400,7 @@ def test_manual_sample_candidate_queue_draft_preview_builds_review_items_without
def test_manual_sample_candidate_queue_draft_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -7535,7 +7541,7 @@ def test_manual_sample_candidate_queue_approval_preview_blocks_write_and_maps_ro
def test_manual_sample_candidate_queue_approval_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -7678,7 +7684,7 @@ def test_manual_sample_candidate_queue_transaction_preview_blocks_execution():
def test_manual_sample_candidate_queue_transaction_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -13401,7 +13407,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -13476,7 +13482,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -13748,7 +13754,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -14036,7 +14042,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -14346,7 +14352,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -14649,7 +14655,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -14908,7 +14914,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -15181,7 +15187,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -15429,7 +15435,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -15659,7 +15665,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -15896,7 +15902,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -16138,7 +16144,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -16414,7 +16420,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -16691,7 +16697,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -16970,7 +16976,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -17299,7 +17305,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -17561,7 +17567,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -17825,7 +17831,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -18095,7 +18101,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -18383,7 +18389,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -18670,7 +18676,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -18744,7 +18750,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_
def test_candidate_queue_writer_preflight_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -18799,7 +18805,7 @@ def test_candidate_queue_writer_status_route_never_leaks_approval_token(monkeypa
monkeypatch.setenv("MARKET_INTEL_QUEUE_WRITE_APPROVAL", TEST_APPROVAL_TOKEN)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -18864,7 +18870,7 @@ def test_candidate_queue_writer_status_route_never_leaks_approval_token(monkeypa
def test_candidate_queue_writer_status_blocks_invalid_payload():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -18889,7 +18895,7 @@ def test_candidate_queue_writer_status_blocks_invalid_payload():
def test_candidate_queue_writer_postwrite_smoke_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -18943,7 +18949,7 @@ def test_candidate_queue_writer_postwrite_smoke_route_is_post_only_and_no_write(
def test_candidate_queue_writer_operator_drill_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -18999,7 +19005,7 @@ def test_candidate_queue_writer_operator_drill_route_is_post_only_and_no_write()
def test_candidate_queue_writer_run_package_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -19058,7 +19064,7 @@ def test_candidate_queue_writer_run_package_route_is_post_only_and_no_write():
def test_candidate_queue_writer_run_readiness_route_is_post_only_and_no_write():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -19347,7 +19353,7 @@ def test_candidate_queue_writer_run_receipt_route_accepts_inline_payload_no_writ
sample_result=sample_result
)
expected_key = transaction["statements"][0]["lookup"]["dedupe_key"]
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -19427,7 +19433,7 @@ def test_candidate_queue_writer_run_closeout_route_is_post_only_and_no_write():
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-closeout-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -19477,7 +19483,7 @@ def test_candidate_queue_review_handoff_route_is_post_only_and_no_write():
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-handoff-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -19528,7 +19534,7 @@ def test_candidate_queue_review_inventory_route_is_post_only_and_no_write():
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-inventory-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -19590,7 +19596,7 @@ def test_candidate_queue_review_decision_route_is_post_only_and_no_write():
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -19657,7 +19663,7 @@ def test_candidate_queue_review_decision_approval_route_is_post_only_and_no_writ
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-approval-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -19730,7 +19736,7 @@ def test_candidate_queue_review_decision_transaction_route_is_post_only_and_no_w
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-transaction-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -19811,7 +19817,7 @@ def test_candidate_queue_review_decision_writer_status_route_is_post_only_and_no
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-writer-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -19895,7 +19901,7 @@ def test_candidate_queue_review_decision_writer_preflight_route_is_post_only_and
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-preflight-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -19979,7 +19985,7 @@ def test_candidate_queue_review_decision_writer_postwrite_smoke_route_is_post_on
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-postwrite-smoke-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20058,7 +20064,7 @@ def test_candidate_queue_review_decision_writer_operator_drill_route_is_post_onl
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-operator-drill-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20144,7 +20150,7 @@ def test_candidate_queue_review_decision_writer_run_package_route_is_post_only_a
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-run-package-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20228,7 +20234,7 @@ def test_candidate_queue_review_decision_writer_run_readiness_route_is_post_only
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-run-readiness-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20317,7 +20323,7 @@ def test_candidate_queue_review_decision_writer_run_receipt_route_is_post_only_a
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-run-receipt-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20427,7 +20433,7 @@ def test_candidate_queue_review_decision_writer_run_closeout_route_is_post_only_
fixture = _build_candidate_queue_writer_receipt_fixture(
"sample-batch-review-decision-run-closeout-route"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20512,7 +20518,7 @@ def test_candidate_queue_review_decision_post_closeout_inventory_route_is_post_o
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20568,7 +20574,7 @@ def test_candidate_queue_review_completion_archive_route_is_post_only_and_no_wri
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20621,7 +20627,7 @@ def test_candidate_queue_review_archive_summary_route_is_post_only_and_no_write(
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20682,7 +20688,7 @@ def test_candidate_queue_review_ai_summary_preflight_route_is_post_only_and_no_w
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20751,7 +20757,7 @@ def test_candidate_queue_review_ai_summary_run_package_route_is_post_only_and_no
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20823,7 +20829,7 @@ def test_candidate_queue_review_ai_summary_output_receipt_route_is_post_only_and
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20893,7 +20899,7 @@ def test_candidate_queue_review_ai_summary_persistence_preflight_route_is_post_o
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -20964,7 +20970,7 @@ def test_candidate_queue_review_ai_summary_persistence_transaction_route_is_post
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21029,7 +21035,7 @@ def test_candidate_queue_review_ai_summary_persistence_writer_preflight_route_is
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21100,7 +21106,7 @@ def test_candidate_queue_review_ai_summary_persistence_run_package_route_is_post
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21173,7 +21179,7 @@ def test_candidate_queue_review_ai_summary_persistence_run_readiness_route_is_po
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21250,7 +21256,7 @@ def test_candidate_queue_review_ai_summary_persistence_run_receipt_route_is_post
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21327,7 +21333,7 @@ def test_candidate_queue_review_ai_summary_persistence_run_closeout_route_is_pos
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21404,7 +21410,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate_ro
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21478,7 +21484,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_pac
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21557,7 +21563,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_rea
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21644,7 +21650,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_rec
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21725,7 +21731,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeou
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21807,7 +21813,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive
from routes.market_intel_routes import market_intel_bp
from routes.market_intel_review_routes import market_intel_review_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
app.register_blueprint(market_intel_review_bp)
@@ -21895,7 +21901,7 @@ def test_candidate_queue_writer_run_receipt_route_is_post_only_and_no_write():
fixture = _build_candidate_queue_writer_receipt_fixture(
batch_id="sample-batch-route-receipt"
)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -21968,7 +21974,7 @@ def test_scheduler_plan_preview_blocks_job_attachment():
def test_scheduler_plan_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -22015,7 +22021,7 @@ def test_match_review_plan_preview_blocks_auto_confirm():
def test_match_review_plan_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -22067,7 +22073,7 @@ def test_opportunity_plan_preview_blocks_alerts_and_ai_summary():
def test_opportunity_plan_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -22126,7 +22132,7 @@ def test_opportunity_scoring_plan_preview_blocks_scoring_and_alerts():
def test_opportunity_scoring_plan_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -22184,7 +22190,7 @@ def test_opportunity_evidence_plan_preview_blocks_queries_and_alerts():
def test_opportunity_evidence_plan_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -22282,7 +22288,7 @@ def test_opportunity_alert_plan_preview_blocks_dispatch_and_llm_calls():
def test_opportunity_alert_plan_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -22360,7 +22366,7 @@ def test_mcp_deploy_preflight_ready_when_env_contract_is_present():
def test_mcp_deploy_preflight_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -27469,7 +27475,7 @@ def test_migration_apply_drill_sqlite_read_only_reports_already_applied():
def test_migration_apply_drill_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -27596,7 +27602,7 @@ def test_migration_catalog_review_sqlite_read_only_detects_partial_schema():
def test_migration_catalog_review_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -27671,7 +27677,7 @@ def test_migration_live_smoke_sqlite_not_applied_tolerates_seed_table_missing():
def test_migration_live_smoke_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -27828,7 +27834,7 @@ def test_live_db_inventory_sqlite_read_only_counts_market_tables():
def test_live_db_inventory_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -27898,7 +27904,7 @@ def test_seed_writer_cli_status_route_never_leaks_approval_token(monkeypatch):
monkeypatch.setenv("MARKET_INTEL_SEED_WRITE_APPROVAL", TEST_APPROVAL_TOKEN)
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -28243,7 +28249,7 @@ def test_mcp_professional_source_governance_blocks_unsafe_source():
def test_mcp_professional_source_governance_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
@@ -28368,7 +28374,7 @@ def test_mcp_fetch_target_source_governance_review_blocks_missing_source_contrac
def test_mcp_fetch_target_source_governance_review_route_get_and_post_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app = _build_test_app()
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()

View File

@@ -270,7 +270,24 @@ def test_pchome_growth_opportunities_use_plain_language_and_pause_shopee_coupang
assert payload["stats"]["needs_mapping_count"] == 1
assert payload["stats"]["catalog_product_count"] == 2
assert payload["stats"]["mapped_product_count"] == 1
assert payload["stats"]["active_catalog_product_count"] == 2
assert payload["stats"]["active_mapped_product_count"] == 1
assert payload["stats"]["active_catalog_mapping_rate"] == 50.0
assert payload["stats"]["sales_freshness"]["status"] == "critical"
contract = payload["stats"]["comparison_metric_contract"]
assert contract["numerator"] == 1
assert contract["denominator"] == 2
assert contract["state_partition"] == {
"comparison_ready": 1,
"candidate_validation": 0,
"unmatched": 1,
"total": 2,
"mutually_exclusive": True,
}
assert contract["operational_decision"]["ready"] is False
assert contract["operational_decision"]["ready_count"] == 0
assert "sales_data_stale" in contract["operational_decision"]["blocked_reason_codes"]
assert contract["platform_runtime"]["denominator"] == 15
actions = {item["pchome_product_id"]: item["recommended_action"]["label"] for item in payload["opportunities"]}
assert actions["PCH-1"] == "檢查售價與活動"
@@ -282,6 +299,33 @@ def test_pchome_growth_opportunities_use_plain_language_and_pause_shopee_coupang
assert all("identity" not in " ".join(item["reason_lines"]).lower() for item in payload["opportunities"])
def test_pchome_growth_coverage_states_are_mutually_exclusive():
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
engine = create_engine("sqlite:///:memory:")
_seed_growth_tables(engine)
_seed_growth_review_candidate_offer(engine)
with engine.begin() as conn:
conn.execute(text("""
INSERT INTO daily_sales_snapshot
("商品ID", "商品名稱", snapshot_date, "總業績", "數量", "商品館")
VALUES
('PCH-REVIEW', 'PChome 待確認商品', '2026-06-14', 100000, 20, '彩妝')
"""))
payload = build_pchome_growth_opportunities(engine, limit=5)
stats = payload["stats"]
assert stats["candidate_count"] == 3
assert stats["comparison_ready_count"] == 1
assert stats["current_review_candidate_count"] == 1
assert stats["candidate_validation_count"] == 1
assert stats["unmatched_count"] == 1
assert stats["unresolved_count"] == 2
assert stats["comparison_ready_count"] + stats["candidate_validation_count"] + stats["unmatched_count"] == 3
assert stats["comparison_metric_contract"]["state_partition"]["mutually_exclusive"] is True
def test_momo_review_candidates_return_dual_store_links_and_plain_reasons():
from services.external_market_offer_service import list_momo_review_candidates

View File

@@ -2,6 +2,7 @@ import importlib
import inspect
import ast
import json
import threading
from datetime import datetime
from pathlib import Path
@@ -229,8 +230,9 @@ def test_roi_ai_smoke_and_daily_report_schedules_stay_staggered():
assert 'schedule.every().day.at("09:05").do(run_roi_monthly_report_if_new_month)' in source
assert 'schedule.every().day.at("09:10").do(run_ai_smoke_daily_summary_task)' in source
assert 'schedule.every().day.at("10:30").do(run_pchome_match_backfill_task)' in source
assert 'schedule.every().day.at("10:45").do(run_pchome_growth_momo_backfill_task)' in source
assert "schedule.every(30).minutes.do(run_pchome_growth_momo_backfill_catchup_task)" in source
assert 'schedule.every(30).minutes.do(run_revenue_automation_cycle_background_task)' in source
assert 'schedule.every().day.at("10:45").do(run_pchome_growth_momo_backfill_background_task)' in source
assert "start_revenue_automation_watchdog()" in source
assert "schedule.every(6).hours.do(run_action_plan_hygiene_task)" in source
assert "schedule.every(15).minutes.do(run_ollama_111_usage_guard_check)" in source
@@ -275,6 +277,63 @@ def test_pchome_growth_backfill_catchup_executes_missing_receipt(monkeypatch, tm
assert calls == ["executed"]
def test_revenue_automation_cycle_runs_sales_refresh_before_mapping(monkeypatch):
run_scheduler = _load_run_scheduler(monkeypatch)
calls = []
monkeypatch.setattr(run_scheduler, "run_auto_import_task", lambda: calls.append("sales"))
monkeypatch.setattr(
run_scheduler,
"run_pchome_growth_momo_backfill_catchup_task",
lambda: calls.append("mapping") or {"status": "executed"},
)
result = run_scheduler.run_revenue_automation_cycle_task()
assert calls == ["sales", "mapping"]
assert result == {"status": "completed", "backfill": {"status": "executed"}}
def test_revenue_automation_background_is_nonblocking_and_rejects_overlap(monkeypatch):
run_scheduler = _load_run_scheduler(monkeypatch)
monkeypatch.setattr(run_scheduler, "_REVENUE_AUTOMATION_LOCK", threading.Lock())
threads = []
calls = []
class DeferredThread:
def __init__(self, *, target, daemon, name):
self.target = target
self.daemon = daemon
self.name = name
self.started = False
threads.append(self)
def start(self):
self.started = True
monkeypatch.setattr(run_scheduler.threading, "Thread", DeferredThread)
queued = run_scheduler._launch_revenue_automation_background(
"coverage-refresh",
lambda: calls.append("executed"),
)
overlapping = run_scheduler._launch_revenue_automation_background(
"duplicate",
lambda: calls.append("duplicate"),
)
assert queued == {"status": "queued", "job": "coverage-refresh"}
assert overlapping == {
"status": "skipped",
"reason": "revenue_automation_already_running",
}
assert threads[0].started is True
assert calls == []
threads[0].target()
assert calls == ["executed"]
assert run_scheduler._REVENUE_AUTOMATION_LOCK.locked() is False
def test_ollama_111_usage_guard_stays_observational(monkeypatch):
run_scheduler = _load_run_scheduler(monkeypatch)
source = inspect.getsource(run_scheduler.run_ollama_111_usage_guard_check)

View File

@@ -24,11 +24,17 @@ def test_security_governance_review_is_honest_and_release_gate_passes():
assert report["completion"]["asset_runtime_reconciliation_complete"] is False
assert report["completion"]["runtime_closure_passed"] < report["completion"]["runtime_closure_total"]
assert report["work_items"][0]["id"] == "SEC-P0-001"
assert report["work_items"][1]["id"] == "SEC-P0-002"
assert report["work_items"][1]["status"] == "in_progress"
assert len(report["work_items"]) == 22
assert report["work_items"][9]["id"] == "REL-P0-001"
assert any(item["id"] == "QA-P1-001" for item in report["work_items"])
assert report["work_items"][-1]["id"] == "CHAOS-P2-001"
checks = {item["id"]: item for item in report["checks"]}
assert checks["PR.AC-database-identity-rbac"]["status"] == "partial"
identity_markers = checks["PR.AC-database-identity-rbac"]["evidence"]["source"]["markers"]
assert identity_markers["identity_mutation_audit"] is True
assert identity_markers["permission_mutation_audit"] is True
access = checks["PR.AA-route-access-control"]["evidence"]
assert access["mutating_unguarded_count"] == 0
assert access["unclassified_unguarded_count"] == 0
@@ -211,6 +217,41 @@ def test_runtime_receipt_rejects_public_metrics_even_when_other_checks_pass(tmp_
assert receipt["security_readback_passed"] is False
def test_auth_identity_runtime_receipt_requires_database_only_closure(tmp_path):
from services.security_governance_review_service import (
_auth_identity_runtime_receipt,
)
governance = tmp_path / "governance"
governance.mkdir()
path = governance / "auth_identity_runtime_receipt.json"
payload = {
"receipt_kind": "auth_identity_runtime_readback",
"work_item_id": "SEC-P0-002",
"status": "canary_ready",
"auth_identity_mode": "hybrid",
"canary_ready": True,
"runtime_closed": False,
"inventory": {"users_total": 2, "active_admin_count": 2},
"checks": {"legacy_shared_password_retired": False},
}
path.write_text(json.dumps(payload), encoding="utf-8")
canary = _auth_identity_runtime_receipt(tmp_path)
assert canary["canary_ready"] is True
assert canary["runtime_closed"] is False
payload.update({
"status": "completed",
"auth_identity_mode": "database",
"runtime_closed": True,
"checks": {"legacy_shared_password_retired": True},
})
path.write_text(json.dumps(payload), encoding="utf-8")
closed = _auth_identity_runtime_receipt(tmp_path)
assert closed["runtime_closed"] is True
assert closed["legacy_shared_password_retired"] is True
def test_sensitive_blueprint_policy_denies_anonymous_api(monkeypatch):
import auth
from services.http_access_policy_service import enforce_http_access_policy
@@ -234,6 +275,9 @@ def test_sensitive_blueprint_policy_denies_anonymous_api(monkeypatch):
with client.session_transaction() as session:
session["logged_in"] = True
session["role"] = "admin"
session["username"] = "legacy-admin"
session["auth_source"] = "legacy_shared_password"
authenticated = client.get("/vendor-stockout/api/vendor/list")
assert authenticated.status_code == 200