2525 lines
99 KiB
Python
2525 lines
99 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import signal
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
from datetime import datetime, timezone
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
from threading import Thread
|
|
from types import ModuleType, SimpleNamespace
|
|
|
|
import pytest
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
|
|
from rc06_environment_test_support import rc06_environment_values
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def load_script(name: str, relative_path: str) -> ModuleType:
|
|
path = REPO_ROOT / relative_path
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
preflight = load_script("yuqei_production_preflight", "yuqei-infra/deploy/preflight-production.py")
|
|
backup_run = load_script("yuqei_backup_run", "yuqei-infra/backup/run-production-backup.py")
|
|
backup_verify = load_script("yuqei_backup_verify", "yuqei-infra/backup/verify-production-backup.py")
|
|
snapshot_restore_planner = load_script(
|
|
"yuqei_snapshot_restore_planner_for_backup",
|
|
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
|
|
)
|
|
provider_acceptance = load_script("yuqei_provider_acceptance", "yuqei-infra/scripts/run-ai-provider-acceptance.py")
|
|
contract_product_golden = load_script(
|
|
"yuqei_contract_product_golden",
|
|
"yuqei-infra/scripts/run-contract-product-golden.py",
|
|
)
|
|
release_quality_gate = load_script(
|
|
"yuqei_release_quality_gate",
|
|
"yuqei-infra/scripts/run-release-quality-gate.py",
|
|
)
|
|
|
|
from yuqei_ai_platform_api.provider_acceptance_receipt import ( # noqa: E402
|
|
RECEIPT_DOMAIN,
|
|
ProviderAcceptanceReceiptPayload,
|
|
SignedProviderAcceptanceReceipt,
|
|
canonical_receipt_payload,
|
|
)
|
|
|
|
|
|
def production_values() -> dict[str, str]:
|
|
values = preflight.parse_env_file(REPO_ROOT / "yuqei-infra/compose/.env.production.example")
|
|
replacements = {
|
|
"RELEASE_ID": "v2.0.0-rc.1",
|
|
"YUQEI_RELEASE_ENVIRONMENT_ID": "production-primary",
|
|
"YUQEI_RELEASE_HOST_ID": "production-host-01",
|
|
"CONTRACT_API_IMAGE": f"registry.internal/contract-api@sha256:{'1' * 64}",
|
|
"CONTRACT_WEB_IMAGE": f"registry.internal/contract-web@sha256:{'c' * 64}",
|
|
"AI_PLATFORM_API_IMAGE": f"registry.internal/ai-platform-api@sha256:{'2' * 64}",
|
|
"AI_PLATFORM_RELEASE_IMAGE_DIGEST": f"sha256:{'2' * 64}",
|
|
"CONTRACT_API_PUBLIC_POLICY_RELEASE_DIGEST": f"sha256:{'1' * 64}",
|
|
"AI_WORKER_IMAGE": f"registry.internal/ai-worker@sha256:{'3' * 64}",
|
|
"POSTGRES_IMAGE": f"registry.internal/pgvector@sha256:{'4' * 64}",
|
|
"POSTGRES_CLIENT_IMAGE": f"registry.internal/postgres@sha256:{'5' * 64}",
|
|
"REDIS_IMAGE": f"registry.internal/redis@sha256:{'6' * 64}",
|
|
"MINIO_IMAGE": f"registry.internal/rustfs@sha256:{'7' * 64}",
|
|
"MINIO_MC_IMAGE": f"registry.internal/rustfs-rc@sha256:{'8' * 64}",
|
|
"NGINX_IMAGE": f"registry.internal/nginx@sha256:{'9' * 64}",
|
|
"BACKUP_UTILITY_IMAGE": f"registry.internal/alpine@sha256:{'a' * 64}",
|
|
"BACKEND_SERVER_NAME": "api.contracts.acme.cn",
|
|
"BACKEND_PUBLIC_BASE_URL": "https://api.contracts.acme.cn",
|
|
"POSTGRES_USER": "yuqei_prod",
|
|
"POSTGRES_PASSWORD": "postgres-secret-that-is-long-and-unique-001",
|
|
"MINIO_ROOT_USER": "yuqei_prod_minio",
|
|
"MINIO_ROOT_PASSWORD": "minio-secret-that-is-long-and-unique-002",
|
|
"CONTRACT_API_WEB_PROXY_SHARED_SECRET": "proxy-secret-that-is-long-and-unique-003",
|
|
"CONTRACT_API_IDENTITY_RATE_LIMIT_SECRET": "identity-secret-that-is-long-and-unique-004",
|
|
"CONTRACT_API_ANTI_ABUSE_HASH_SECRET": "contract-anti-abuse-secret-that-is-long-004a", # pragma: allowlist secret
|
|
"CONTRACT_WEB_SESSION_SECRET": "session-secret-that-is-long-and-unique-005",
|
|
"AI_PLATFORM_SERVICE_SHARED_SECRET": "ai-service-secret-that-is-long-and-unique-006",
|
|
"KMS_GATEWAY_IMAGE": f"registry.internal/kms-gateway@sha256:{'b' * 64}",
|
|
"KMS_GATEWAY_ALLOWED_KEY_IDS": "calendar-envelope-key-v1",
|
|
"KMS_GATEWAY_PRIMARY_KEY_ID": "calendar-envelope-key-v1",
|
|
"KMS_GATEWAY_ALLOWED_SIGNING_KEY_IDS": "reliable-evidence-signing-key-v1",
|
|
"KMS_GATEWAY_SIGNING_KEY_ID": "reliable-evidence-signing-key-v1",
|
|
"KMS_GATEWAY_SIGNING_KEY_VERSION": "version-2026-08-25",
|
|
"ALIYUN_KMS_ENDPOINT": "kst-production.cryptoservice.kms.aliyuncs.com",
|
|
"AI_PLATFORM_GENERATIVE_AI_FILING_NUMBER": "内网信算备-TEST-001",
|
|
"AI_PLATFORM_GENERATIVE_AI_SERVICE_REGISTRATION_NUMBER": "内网信生测评-TEST-001",
|
|
"AI_PLATFORM_GENERATIVE_AI_SERVICE_PROVIDER_CODE": "yuqei-test-provider-code",
|
|
"AI_PLATFORM_PROVIDER_ACCEPTANCE_EXPECTED_RECEIPT_SHA256": hashlib.sha256(
|
|
b"pinned-production-provider-acceptance-receipt"
|
|
).hexdigest(),
|
|
"PRODUCTION_ALERT_WEBHOOK_URL_FILE": "/etc/yuqei/monitoring/alert-webhook-url",
|
|
"AI_PLATFORM_ANTI_ABUSE_HASH_SECRET": "ai-anti-abuse-secret-that-is-long-and-unique-006a", # pragma: allowlist secret
|
|
"CONTRACT_API_DATABASE_URL": "postgresql+psycopg://yuqei_prod:encoded-password@postgres:5432/yuqei_contract",
|
|
"AI_PLATFORM_DATABASE_URL": "postgresql+psycopg://yuqei_prod:encoded-password@postgres:5432/yuqei_ai",
|
|
"YUQEI_WORKER_DATABASE_URL": "postgresql+psycopg://yuqei_prod:encoded-password@postgres:5432/yuqei_ai",
|
|
}
|
|
values.update(replacements)
|
|
values.update(
|
|
rc06_environment_values(
|
|
contract_digest="sha256:" + "1" * 64,
|
|
ai_digest=values["AI_PLATFORM_RELEASE_IMAGE_DIGEST"],
|
|
)
|
|
)
|
|
return values
|
|
|
|
|
|
def test_object_storage_backup_injects_rc_config_via_stdin_without_secret_argv(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
partial_dir = tmp_path / ".backup-20260811T120000Z.partial"
|
|
values = {
|
|
"COMPOSE_PROJECT_NAME": "yuqei-v2-production",
|
|
"AI_PLATFORM_OBJECT_STORAGE_BUCKET": "ai-platform-uploads",
|
|
"MINIO_ROOT_USER": "backup-access-key",
|
|
"MINIO_ROOT_PASSWORD": "backup-secret-value",
|
|
"MINIO_MC_IMAGE": f"rustfs/rc@sha256:{'a' * 64}",
|
|
}
|
|
command = backup_run.object_storage_mirror_command(
|
|
values=values,
|
|
partial_dir=partial_dir,
|
|
)
|
|
config = backup_run._rc_config_bytes(values).decode("utf-8")
|
|
|
|
assert command[:2] == ["docker", "run"]
|
|
assert command[command.index("--pull") + 1] == "never"
|
|
assert "--interactive" in command
|
|
assert command[command.index("--entrypoint") + 1] == "/bin/sh"
|
|
assert "--env-file" not in command
|
|
assert "HOME=/tmp/rc-home" in command
|
|
assert command[-3:] == [
|
|
"--",
|
|
"source/ai-platform-uploads/",
|
|
"/backup/minio/",
|
|
]
|
|
read_only_index = command.index("--read-only")
|
|
assert ["--read-only", "--cap-drop", "ALL"] == command[
|
|
read_only_index : read_only_index + 3
|
|
]
|
|
assert "no-new-privileges:true" in command
|
|
assert "cat > \"$HOME/.config/rc/config.toml\"" in command[-4]
|
|
assert "exec rc --no-color --no-progress mirror --overwrite" in command[-4]
|
|
assert all("backup-secret-value" not in token for token in command)
|
|
assert all("backup-access-key" not in token for token in command)
|
|
assert 'name = "source"' in config
|
|
assert 'endpoint = "http://minio:9000"' in config
|
|
assert 'access_key = "backup-access-key"' in config
|
|
assert 'secret_key = "backup-secret-value"' in config
|
|
|
|
|
|
def test_object_storage_backup_rejects_unsafe_bind_source(tmp_path: Path) -> None:
|
|
with pytest.raises(RuntimeError, match="unsafe character"):
|
|
backup_run._docker_bind_source(tmp_path / "comma,path")
|
|
|
|
|
|
def test_backup_quiesces_every_long_running_application_writer() -> None:
|
|
assert backup_run.WRITER_SERVICES == (
|
|
"contract-api",
|
|
"contract-review-worker",
|
|
"contract-playbook-governance-worker",
|
|
"ai-platform-api",
|
|
"worker",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("label", "validation_name", "table_name"),
|
|
[
|
|
("contract", "contract_api_alembic_version", "contract_api_alembic_version"),
|
|
("ai_platform", "ai_platform_alembic_version", "ai_platform_alembic_version"),
|
|
("operations", "public_schema_fingerprint", "information_schema.columns"),
|
|
],
|
|
)
|
|
def test_restore_check_uses_each_database_owned_schema_marker(
|
|
label: str,
|
|
validation_name: str,
|
|
table_name: str,
|
|
) -> None:
|
|
actual_name, query = backup_verify.restore_validation_query(label)
|
|
|
|
assert actual_name == validation_name
|
|
assert table_name in query
|
|
|
|
|
|
def test_production_preflight_accepts_strict_configuration() -> None:
|
|
checks = preflight.validate_environment(production_values())
|
|
|
|
assert [check.key for check in checks if check.status == "fail"] == []
|
|
|
|
|
|
def test_production_launch_scope_excludes_support_access_and_identity_execution() -> None:
|
|
manifest = json.loads(preflight.LAUNCH_SCOPE_MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
|
|
assert manifest == preflight.EXPECTED_LAUNCH_SCOPE
|
|
assert manifest["decision"] == "excluded"
|
|
assert manifest["support_access"] == {
|
|
"included": False,
|
|
"enabled": False,
|
|
"kill_switch_engaged": True,
|
|
"executor_enabled": False,
|
|
}
|
|
assert manifest["identity_executor"] == {"enabled": False}
|
|
assert manifest["destructive_identity_write_api"]["included"] is False
|
|
assert set(manifest["destructive_identity_write_api"]["forbidden_operations"]) == {
|
|
"execute",
|
|
"grant",
|
|
"revoke",
|
|
"impersonate",
|
|
"impersonation",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"activation_key",
|
|
sorted(preflight.FORBIDDEN_SUPPORT_ACCESS_ACTIVATION_KEYS),
|
|
)
|
|
def test_production_preflight_rejects_support_access_or_identity_write_activation(
|
|
activation_key: str,
|
|
) -> None:
|
|
values = production_values()
|
|
values[activation_key] = "true"
|
|
|
|
checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
|
|
assert checks["launch_scope_activation_flags"].status == "fail"
|
|
assert activation_key in checks["launch_scope_activation_flags"].message
|
|
|
|
|
|
@pytest.mark.parametrize("artifact_state", ["missing", "tampered", "invalid"])
|
|
def test_production_preflight_fails_closed_on_launch_scope_artifact_drift(
|
|
tmp_path: Path,
|
|
artifact_state: str,
|
|
) -> None:
|
|
manifest_path = tmp_path / "launch-scope.json"
|
|
if artifact_state == "tampered":
|
|
tampered = dict(preflight.EXPECTED_LAUNCH_SCOPE)
|
|
tampered["support_access"] = {
|
|
**tampered["support_access"],
|
|
"enabled": True,
|
|
}
|
|
manifest_path.write_text(json.dumps(tampered), encoding="utf-8")
|
|
elif artifact_state == "invalid":
|
|
manifest_path.write_text("not-json", encoding="utf-8")
|
|
|
|
checks = {
|
|
check.key: check
|
|
for check in preflight._validate_launch_scope_exclusion(
|
|
production_values(),
|
|
manifest_path=manifest_path,
|
|
)
|
|
}
|
|
|
|
assert checks["launch_scope_support_access_exclusion"].status == "fail"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("contract_timeout", "provider_timeout"),
|
|
[("30", "30"), ("0", "30"), ("601", "30"), ("NaN", "30"), ("60", "inf")],
|
|
)
|
|
def test_production_preflight_rejects_invalid_ai_timeout_budgets(
|
|
contract_timeout: str,
|
|
provider_timeout: str,
|
|
) -> None:
|
|
values = production_values()
|
|
values["CONTRACT_API_AI_PLATFORM_TIMEOUT_SECONDS"] = contract_timeout
|
|
values["AI_PLATFORM_PROVIDER_REQUEST_TIMEOUT_SECONDS"] = provider_timeout
|
|
|
|
checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
|
|
assert checks["ai_timeout_budget"].status == "fail"
|
|
|
|
|
|
def test_production_preflight_rejects_promotion_image_digest_drift() -> None:
|
|
values = production_values()
|
|
values["AI_PLATFORM_RELEASE_IMAGE_DIGEST"] = f"sha256:{'f' * 64}"
|
|
|
|
checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
|
|
assert checks["provider_promotion_image_binding"].status == "fail"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"forbidden_key",
|
|
[
|
|
"AI_PLATFORM_PROVIDER_LEGAL_ATTESTATION_SECRET",
|
|
"AI_PLATFORM_PROVIDER_ACCEPTANCE_SIGNING_PRIVATE_KEY_FILE",
|
|
"AI_PLATFORM_PROVIDER_ACCEPTANCE_SIGNING_KEY",
|
|
"AI_PLATFORM_PROVIDER_ACCEPTANCE_STAGING_DATABASE_URL",
|
|
"SHARED_STAGING_POSTGRES_PASSWORD",
|
|
],
|
|
)
|
|
def test_production_preflight_rejects_promotion_private_or_staging_credentials(
|
|
forbidden_key: str,
|
|
) -> None:
|
|
values = production_values()
|
|
values[forbidden_key] = "must-not-enter-production"
|
|
|
|
checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
|
|
assert checks["provider_acceptance_credential_isolation"].status == "fail"
|
|
assert forbidden_key in checks["provider_acceptance_credential_isolation"].message
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("key", "value"),
|
|
[
|
|
("RELEASE_ID", "release id with spaces"),
|
|
("RELEASE_ID", "../release"),
|
|
("AI_PLATFORM_PROVIDER_ACCEPTANCE_EXPECTED_RECEIPT_SHA256", "0" * 64),
|
|
("AI_PLATFORM_PROVIDER_ACCEPTANCE_EXPECTED_RECEIPT_SHA256", "A" * 64),
|
|
("AI_PLATFORM_PROVIDER_ACCEPTANCE_RECEIPT_MAX_TTL_SECONDS", "604801"),
|
|
("AI_PLATFORM_PROVIDER_ACCEPTANCE_RECEIPT_MAX_TTL_SECONDS", "not-a-number"),
|
|
("AI_PLATFORM_PROVIDER_ACCEPTANCE_RECEIPT_CLOCK_SKEW_SECONDS", "301"),
|
|
("AI_PLATFORM_PROVIDER_ACCEPTANCE_RECEIPT_CLOCK_SKEW_SECONDS", "-1"),
|
|
("AI_PLATFORM_PROVIDER_ACCEPTANCE_RECEIPT_FILE_HOST", "relative/receipt.json"),
|
|
(
|
|
"AI_PLATFORM_PROVIDER_ACCEPTANCE_TRUSTED_KEYS_FILE_HOST",
|
|
"/etc/yuqei/provider-acceptance/receipt.json",
|
|
),
|
|
],
|
|
)
|
|
def test_production_preflight_rejects_unpinned_or_ambiguous_promotion_config(
|
|
key: str,
|
|
value: str,
|
|
) -> None:
|
|
values = production_values()
|
|
values[key] = value
|
|
|
|
failed = {check.key for check in preflight.validate_environment(values) if check.status == "fail"}
|
|
|
|
assert failed & {"release_id", "provider_acceptance_deployment"}
|
|
|
|
|
|
def test_provider_acceptance_promotion_files_are_present_and_hash_pinned(tmp_path: Path) -> None:
|
|
receipt_path = tmp_path / "receipt.json"
|
|
receipt_path.write_bytes(b'{"receipt":"signed"}\n')
|
|
trusted_keys_path = tmp_path / "trusted-keys.json"
|
|
trusted_keys_path.write_bytes(b'{"version":1,"keys":[]}\n')
|
|
values = production_values()
|
|
values.update(
|
|
{
|
|
"AI_PLATFORM_PROVIDER_ACCEPTANCE_RECEIPT_FILE_HOST": str(receipt_path.resolve()),
|
|
"AI_PLATFORM_PROVIDER_ACCEPTANCE_TRUSTED_KEYS_FILE_HOST": str(
|
|
trusted_keys_path.resolve()
|
|
),
|
|
"AI_PLATFORM_PROVIDER_ACCEPTANCE_EXPECTED_RECEIPT_SHA256": hashlib.sha256(
|
|
receipt_path.read_bytes()
|
|
).hexdigest(),
|
|
}
|
|
)
|
|
|
|
checks = {check.key: check for check in preflight.validate_files(values)}
|
|
|
|
assert checks["provider_acceptance_receipt_file"].status == "pass"
|
|
assert checks["provider_acceptance_receipt_hash"].status == "pass"
|
|
assert checks["provider_acceptance_trusted_keys_file"].status == "pass"
|
|
|
|
receipt_path.write_bytes(b'{"receipt":"tampered"}\n')
|
|
tampered = {check.key: check for check in preflight.validate_files(values)}
|
|
assert tampered["provider_acceptance_receipt_hash"].status == "fail"
|
|
|
|
|
|
def test_production_preflight_requires_owner_only_https_alert_webhook(tmp_path: Path) -> None:
|
|
values = production_values()
|
|
webhook = tmp_path / "alert-webhook-url"
|
|
webhook.write_text("https://alerts.acme.cn/hooks/yuqei\n", encoding="utf-8")
|
|
webhook.chmod(0o600)
|
|
values["PRODUCTION_ALERT_WEBHOOK_URL_FILE"] = str(webhook.resolve())
|
|
|
|
checks = {check.key: check for check in preflight.validate_files(values)}
|
|
|
|
assert checks["alert_webhook_file"].status == "pass"
|
|
if "alert_webhook_permissions" in checks:
|
|
assert checks["alert_webhook_permissions"].status == "pass"
|
|
|
|
webhook.write_text("http://alerts.acme.cn/hooks/yuqei\n", encoding="utf-8")
|
|
assert {check.key: check for check in preflight.validate_files(values)}["alert_webhook_file"].status == "fail"
|
|
|
|
|
|
def test_production_preflight_rejects_missing_alert_webhook_file(tmp_path: Path) -> None:
|
|
values = production_values()
|
|
values["PRODUCTION_ALERT_WEBHOOK_URL_FILE"] = str((tmp_path / "missing").resolve())
|
|
|
|
checks = {check.key: check for check in preflight.validate_files(values)}
|
|
|
|
assert checks["alert_webhook_file"].status == "fail"
|
|
|
|
|
|
def test_production_compose_mounts_only_promotion_verification_material_read_only() -> None:
|
|
override = (
|
|
REPO_ROOT / "yuqei-infra/compose/docker-compose.production.yml"
|
|
).read_text(encoding="utf-8")
|
|
|
|
assert "AI_PLATFORM_RELEASE_ID: ${RELEASE_ID:?" in override
|
|
assert "AI_PLATFORM_RELEASE_IMAGE_DIGEST: ${AI_PLATFORM_RELEASE_IMAGE_DIGEST:?" in override
|
|
assert "AI_PLATFORM_PROVIDER_ACCEPTANCE_EXPECTED_RECEIPT_SHA256: ${" in override
|
|
assert ":/run/yuqei/provider-acceptance/receipt.json:ro" in override
|
|
assert ":/run/yuqei/provider-acceptance/trusted-keys.json:ro" in override
|
|
assert "CONTRACT_API_PUBLIC_POLICY_CANDIDATE_DIR: /run/yuqei/public-policy/candidate" in override
|
|
assert "${PUBLIC_POLICY_CANDIDATE_DIR_HOST:?PUBLIC_POLICY_CANDIDATE_DIR_HOST is required}:/run/yuqei/public-policy/candidate:ro" in override
|
|
assert "${PUBLIC_POLICY_LEGAL_APPROVAL_FILE_HOST:?PUBLIC_POLICY_LEGAL_APPROVAL_FILE_HOST is required}:/run/yuqei/public-policy/named-policy-legal-approval.json:ro" in override
|
|
assert "${PUBLIC_POLICY_TRUSTED_KEYRING_FILE_HOST:?PUBLIC_POLICY_TRUSTED_KEYRING_FILE_HOST is required}:/run/yuqei/public-policy/policy-legal-keyring.trusted.json:ro" in override
|
|
assert "CONTRACT_API_PUBLIC_POLICY_EXECUTOR_ENABLED: ${CONTRACT_API_PUBLIC_POLICY_EXECUTOR_ENABLED:-false}" in override
|
|
assert 'AI_PLATFORM_PROVIDER_LEGAL_ATTESTATION_SECRET: ""' in override
|
|
assert "SIGNING_PRIVATE_KEY" not in override
|
|
assert "STAGING_DATABASE_URL" not in override
|
|
|
|
|
|
def test_production_prompt_asset_import_is_dry_run_by_default() -> None:
|
|
override = (
|
|
REPO_ROOT / "yuqei-infra/compose/docker-compose.production.yml"
|
|
).read_text(encoding="utf-8")
|
|
service = override.split(" ai-platform-prompt-assets-import:", 1)[1].split(
|
|
"\n ai-platform-api:", 1
|
|
)[0]
|
|
|
|
command_line = next(line.strip() for line in service.splitlines() if line.strip().startswith("command:"))
|
|
assert command_line == 'command: ["python", "-m", "yuqei_ai_platform_api.packaged_prompt_assets_import"]'
|
|
|
|
|
|
def _write_public_policy_evidence(tmp_path: Path) -> dict[str, str]:
|
|
candidate = tmp_path / "public-policy"
|
|
candidate.mkdir()
|
|
documents = []
|
|
for policy_id, source_file in (
|
|
("user_terms", "user-terms.md"),
|
|
("privacy_policy", "privacy-policy.md"),
|
|
("service_agreement", "service-agreement.md"),
|
|
("ai_use_notice", "ai-use-notice.md"),
|
|
):
|
|
(candidate / source_file).write_text(f"# {policy_id}\n", encoding="utf-8")
|
|
documents.append({"policy_id": policy_id, "source_file": source_file})
|
|
(candidate / "manifest.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"schema_version": "yuqei-public-policy-candidate-v1",
|
|
"existing_user_reconsent": "required_on_next_login",
|
|
"documents": documents,
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
approval = tmp_path / "policy-approval.json"
|
|
approval.write_text('{"approval_id":"policy-approval"}', encoding="utf-8")
|
|
keyring = tmp_path / "policy-keyring.json"
|
|
keyring.write_text(
|
|
json.dumps({"trust_basis": {"policy_activation_authorized": False}}),
|
|
encoding="utf-8",
|
|
)
|
|
return {
|
|
"PUBLIC_POLICY_CANDIDATE_DIR_HOST": str(candidate.resolve()),
|
|
"PUBLIC_POLICY_LEGAL_APPROVAL_FILE_HOST": str(approval.resolve()),
|
|
"PUBLIC_POLICY_TRUSTED_KEYRING_FILE_HOST": str(keyring.resolve()),
|
|
}
|
|
|
|
|
|
def test_production_preflight_rejects_invalid_public_policy_deployment() -> None:
|
|
values = production_values()
|
|
values["CONTRACT_API_PUBLIC_POLICY_RELEASE_DIGEST"] = f"sha256:{'f' * 64}"
|
|
|
|
checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
|
|
assert checks["public_policy_deployment"].status == "fail"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("key", "value"),
|
|
[
|
|
("PUBLIC_POLICY_CANDIDATE_DIR_HOST", "relative/public-policy"),
|
|
("PUBLIC_POLICY_LEGAL_APPROVAL_FILE_HOST", "/etc/yuqei/public-policy/2026-08-12/policy-legal-keyring.trusted.json"),
|
|
("CONTRACT_API_PUBLIC_POLICY_EXECUTOR_ENABLED", "true"),
|
|
],
|
|
)
|
|
def test_production_preflight_rejects_unsafe_public_policy_paths_or_executor(
|
|
key: str,
|
|
value: str,
|
|
) -> None:
|
|
values = production_values()
|
|
values[key] = value
|
|
|
|
checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
|
|
assert checks["public_policy_deployment"].status == "fail"
|
|
|
|
|
|
def test_production_preflight_rejects_public_policy_private_material() -> None:
|
|
values = production_values()
|
|
values["CONTRACT_API_PUBLIC_POLICY_PRIVATE_KEY_FILE"] = "/etc/yuqei/private.pem"
|
|
|
|
checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
|
|
assert checks["public_policy_credential_isolation"].status == "fail"
|
|
|
|
|
|
def test_public_policy_evidence_files_are_present_and_safe(tmp_path: Path) -> None:
|
|
values = production_values()
|
|
values.update(_write_public_policy_evidence(tmp_path))
|
|
|
|
checks = {check.key: check for check in preflight.validate_files(values)}
|
|
|
|
assert checks["public_policy_candidate_dir"].status == "pass"
|
|
assert checks["public_policy_manifest"].status == "pass"
|
|
assert checks["public_policy_legal_approval_file"].status == "pass"
|
|
assert checks["public_policy_trusted_keyring_file"].status == "pass"
|
|
|
|
|
|
def test_public_policy_preflight_fails_on_manifest_or_keyring_drift(tmp_path: Path) -> None:
|
|
values = production_values()
|
|
values.update(_write_public_policy_evidence(tmp_path))
|
|
candidate = Path(values["PUBLIC_POLICY_CANDIDATE_DIR_HOST"])
|
|
(candidate / "manifest.json").write_text('{"schema_version":"wrong"}', encoding="utf-8")
|
|
keyring = Path(values["PUBLIC_POLICY_TRUSTED_KEYRING_FILE_HOST"])
|
|
keyring.write_text(
|
|
json.dumps({"trust_basis": {"policy_activation_authorized": True}}),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
checks = {check.key: check for check in preflight.validate_files(values)}
|
|
|
|
assert checks["public_policy_manifest"].status == "fail"
|
|
assert checks["public_policy_trusted_keyring_file"].status == "fail"
|
|
|
|
def test_shared_compose_wires_contract_anti_abuse_controls() -> None:
|
|
compose = (REPO_ROOT / "yuqei-infra/compose/docker-compose.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
for variable in (
|
|
"CONTRACT_API_ANTI_ABUSE_MODE",
|
|
"CONTRACT_API_ANTI_ABUSE_HASH_SECRET",
|
|
"CONTRACT_API_ANTI_ABUSE_S3_RECORDS_DAILY_LIMIT",
|
|
"CONTRACT_API_ANTI_ABUSE_S4_RECORDS_DAILY_LIMIT",
|
|
"CONTRACT_API_ANTI_ABUSE_FILES_DAILY_LIMIT",
|
|
"CONTRACT_API_ANTI_ABUSE_BYTES_DAILY_LIMIT",
|
|
):
|
|
assert f"{variable}: ${{{variable}:-" in compose
|
|
|
|
|
|
def test_shared_compose_and_smoke_deploy_wire_contract_ai_timeout() -> None:
|
|
compose = (REPO_ROOT / "yuqei-infra/compose/docker-compose.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
deploy_script = (
|
|
REPO_ROOT / "yuqei-infra/scripts/deploy-v2-smoke-docker.ps1"
|
|
).read_text(encoding="utf-8")
|
|
|
|
assert compose.count("CONTRACT_API_AI_PLATFORM_TIMEOUT_SECONDS: ${") == 3
|
|
assert "CONTRACT_API_AI_PLATFORM_TIMEOUT_SECONDS=240" in deploy_script
|
|
assert "AI_PLATFORM_PROVIDER_REQUEST_TIMEOUT_SECONDS=180" in deploy_script
|
|
|
|
|
|
def test_release_gate_benchmark_defaults_fit_enforced_anti_abuse_window(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(sys, "argv", ["run-release-quality-gate.py"])
|
|
|
|
args = release_quality_gate.parse_args()
|
|
|
|
assert args.benchmark_requests_per_scenario + args.benchmark_warmup + 1 < 20
|
|
assert 1 <= args.benchmark_concurrency <= args.benchmark_requests_per_scenario
|
|
|
|
|
|
def test_legal_qa_edge_timeout_covers_contract_client_maximum() -> None:
|
|
nginx = (
|
|
REPO_ROOT / "yuqei-infra/nginx/templates/default.conf.template"
|
|
).read_text(encoding="utf-8")
|
|
|
|
location = nginx.split("location = /contract/api/v1/legal/qa", 1)[1].split("}", 1)[0]
|
|
assert "proxy_read_timeout 660s;" in location
|
|
assert "proxy_send_timeout 660s;" in location
|
|
assert "proxy_pass http://contract_api_upstream/api/v1/legal/qa;" in location
|
|
|
|
|
|
def test_smoke_review_requests_include_server_authorized_identity_context() -> None:
|
|
deploy_script = (
|
|
REPO_ROOT / "yuqei-infra/scripts/deploy-v2-smoke-docker.ps1"
|
|
).read_text(encoding="utf-8")
|
|
|
|
assert deploy_script.count("-H 'X-Tenant-Id: default'") >= 3
|
|
assert deploy_script.count("-H 'X-User-Id: user-current-admin'") >= 3
|
|
|
|
|
|
def test_disabled_identity_email_does_not_require_smtp_secret_file() -> None:
|
|
values = production_values()
|
|
values.pop("CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE_HOST", None)
|
|
|
|
environment_checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
file_checks = {check.key: check for check in preflight.validate_files(values)}
|
|
|
|
assert environment_checks["identity_email_delivery"].status == "pass"
|
|
assert file_checks["smtp_password_file"].status == "pass"
|
|
|
|
|
|
def test_smtp_identity_email_requires_and_validates_opt_in_secret_file(tmp_path: Path) -> None:
|
|
password_file = tmp_path / "contract-smtp-password"
|
|
password_file.write_text("test-smtp-password\n", encoding="utf-8")
|
|
password_file.chmod(0o600)
|
|
values = production_values()
|
|
values.update(
|
|
{
|
|
"CONTRACT_API_IDENTITY_EMAIL_DELIVERY_MODE": "smtp",
|
|
"CONTRACT_API_IDENTITY_SELF_SERVICE_ENABLED": "true",
|
|
"CONTRACT_API_IDENTITY_MANUAL_INVITATION_ENABLED": "false",
|
|
"CONTRACT_API_IDENTITY_SMTP_HOST": "smtp.mail.acme.cn",
|
|
"CONTRACT_API_IDENTITY_SMTP_USERNAME": "mailer@acme.cn",
|
|
"CONTRACT_API_IDENTITY_SMTP_PASSWORD": "",
|
|
"CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE": "/run/secrets/contract_smtp_password",
|
|
"CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE_HOST": str(password_file.resolve()),
|
|
"CONTRACT_API_IDENTITY_SMTP_FROM_EMAIL": "mailer@acme.cn",
|
|
"CONTRACT_API_IDENTITY_SMTP_USE_STARTTLS": "true",
|
|
"CONTRACT_API_IDENTITY_SMTP_USE_SSL": "false",
|
|
"CONTRACT_API_IDENTITY_PUBLIC_WEB_BASE_URL": "https://contracts.acme.cn",
|
|
}
|
|
)
|
|
|
|
environment_checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
file_checks = {check.key: check for check in preflight.validate_files(values)}
|
|
|
|
assert environment_checks["identity_email_delivery"].status == "pass"
|
|
assert file_checks["smtp_password_file"].status == "pass"
|
|
if "smtp_password_permissions" in file_checks:
|
|
assert file_checks["smtp_password_permissions"].status == "pass"
|
|
|
|
unencrypted = dict(values)
|
|
unencrypted["CONTRACT_API_IDENTITY_SMTP_USE_STARTTLS"] = "false"
|
|
unencrypted["CONTRACT_API_IDENTITY_SMTP_USE_SSL"] = "false"
|
|
unencrypted_checks = {
|
|
check.key: check for check in preflight.validate_environment(unencrypted)
|
|
}
|
|
assert unencrypted_checks["identity_email_delivery"].status == "fail"
|
|
|
|
password_file.unlink()
|
|
missing_file_checks = {check.key: check for check in preflight.validate_files(values)}
|
|
assert missing_file_checks["smtp_password_file"].status == "fail"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("key", "value"),
|
|
[
|
|
("CONTRACT_API_IDENTITY_SMTP_PORT", "0"),
|
|
("CONTRACT_API_IDENTITY_SMTP_PORT", "not-a-port"),
|
|
("CONTRACT_API_IDENTITY_SMTP_TIMEOUT_SECONDS", "0"),
|
|
("CONTRACT_API_IDENTITY_SMTP_TIMEOUT_SECONDS", "121"),
|
|
("CONTRACT_API_IDENTITY_SMTP_FROM_EMAIL", "not-an-email"),
|
|
("CONTRACT_API_IDENTITY_SMTP_HOST", " smtp.mail.acme.cn"),
|
|
("CONTRACT_API_IDENTITY_SMTP_FROM_NAME", "AI Contract\r\nBcc: hidden@example.com"),
|
|
("CONTRACT_API_IDENTITY_SMTP_USE_STARTTLS", "sometimes"),
|
|
],
|
|
)
|
|
def test_smtp_preflight_rejects_runtime_invalid_values(
|
|
tmp_path: Path,
|
|
key: str,
|
|
value: str,
|
|
) -> None:
|
|
password_file = tmp_path / "contract-smtp-password"
|
|
password_file.write_text("test-smtp-password\n", encoding="utf-8")
|
|
values = production_values()
|
|
values.update(
|
|
{
|
|
"CONTRACT_API_IDENTITY_EMAIL_DELIVERY_MODE": "smtp",
|
|
"CONTRACT_API_IDENTITY_SELF_SERVICE_ENABLED": "true",
|
|
"CONTRACT_API_IDENTITY_MANUAL_INVITATION_ENABLED": "false",
|
|
"CONTRACT_API_IDENTITY_SMTP_HOST": "smtp.mail.acme.cn",
|
|
"CONTRACT_API_IDENTITY_SMTP_USERNAME": "mailer@acme.cn",
|
|
"CONTRACT_API_IDENTITY_SMTP_PASSWORD": "",
|
|
"CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE": "/run/secrets/contract_smtp_password",
|
|
"CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE_HOST": str(password_file.resolve()),
|
|
"CONTRACT_API_IDENTITY_SMTP_FROM_EMAIL": "mailer@acme.cn",
|
|
"CONTRACT_API_IDENTITY_SMTP_USE_STARTTLS": "true",
|
|
"CONTRACT_API_IDENTITY_SMTP_USE_SSL": "false",
|
|
"CONTRACT_API_IDENTITY_SMTP_TIMEOUT_SECONDS": "10",
|
|
"CONTRACT_API_IDENTITY_PUBLIC_WEB_BASE_URL": "https://contracts.acme.cn",
|
|
key: value,
|
|
}
|
|
)
|
|
|
|
checks = {check.key: check for check in preflight.validate_environment(values)}
|
|
|
|
assert checks["identity_email_delivery"].status == "fail"
|
|
|
|
|
|
def test_smtp_production_override_mounts_only_the_password_file_secret() -> None:
|
|
override = (
|
|
REPO_ROOT / "yuqei-infra/compose/docker-compose.production.smtp.yml"
|
|
).read_text(encoding="utf-8")
|
|
|
|
assert "CONTRACT_API_IDENTITY_SMTP_PASSWORD: \"\"" in override
|
|
assert "CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE: /run/secrets/contract_smtp_password" in override
|
|
assert "target: contract_smtp_password" in override
|
|
assert "file: ${CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE_HOST:?" in override
|
|
|
|
|
|
def test_production_preflight_rejects_smoke_database_public_bind_and_fallback() -> None:
|
|
values = production_values()
|
|
values["CONTRACT_POSTGRES_DB"] = "yuqei_contract_smoke"
|
|
values["CONTRACT_API_DATABASE_URL"] = (
|
|
"postgresql+psycopg://yuqei_prod:encoded-password@postgres:5432/yuqei_contract_smoke"
|
|
)
|
|
values["POSTGRES_BIND_ADDRESS"] = "0.0.0.0"
|
|
values["AI_PLATFORM_PROVIDER_FALLBACK_ENABLED"] = "true"
|
|
|
|
failed = {check.key for check in preflight.validate_environment(values) if check.status == "fail"}
|
|
|
|
assert {"production_modes", "internal_port_binding", "database_boundaries"} <= failed
|
|
|
|
|
|
def _complete_backup_fixture(tmp_path: Path) -> tuple[Path, dict[str, object]]:
|
|
backup_dir = tmp_path / "backup-20260716T120000Z"
|
|
backup_dir.mkdir(parents=True)
|
|
archive_path = backup_dir / "contract-files.tar.gz"
|
|
with tarfile.open(archive_path, mode="w:gz") as archive:
|
|
content = b"contract"
|
|
info = tarfile.TarInfo("contract-1/document.txt")
|
|
info.size = len(content)
|
|
archive.addfile(info, io.BytesIO(content))
|
|
for label in ("contract", "ai_platform", "operations"):
|
|
(backup_dir / f"postgres-{label}.dump").write_bytes(
|
|
f"postgres-{label}-dump".encode("utf-8")
|
|
)
|
|
minio_object = backup_dir / "minio" / "tenant-a" / "object.bin"
|
|
minio_object.parent.mkdir(parents=True)
|
|
minio_object.write_bytes(b"minio-object")
|
|
artifacts = []
|
|
for path in sorted(backup_dir.rglob("*")):
|
|
if not path.is_file():
|
|
continue
|
|
artifacts.append(
|
|
{
|
|
"path": path.relative_to(backup_dir).as_posix(),
|
|
"size_bytes": path.stat().st_size,
|
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
}
|
|
)
|
|
minio_records = [
|
|
item for item in artifacts if str(item["path"]).startswith("minio/")
|
|
]
|
|
manifest: dict[str, object] = {
|
|
"schema_version": "yuqei-production-backup-artifacts.v2",
|
|
"backup_id": backup_dir.name,
|
|
"created_at": "2026-07-16T12:00:00+00:00",
|
|
"release_id": "release-2026-07-16",
|
|
"compose_project_name": "yuqei-production",
|
|
"consistency_mode": "quiesced",
|
|
"database_names": {
|
|
"contract": "yuqei_contract",
|
|
"ai_platform": "yuqei_ai",
|
|
"operations": "yuqei_ops",
|
|
},
|
|
"images": {
|
|
"contract_api": f"registry.internal/contract-api@sha256:{'1' * 64}",
|
|
"ai_platform_api": f"registry.internal/ai-platform-api@sha256:{'2' * 64}",
|
|
"ai_worker": f"registry.internal/ai-worker@sha256:{'3' * 64}",
|
|
},
|
|
"object_storage_mirror": {
|
|
"engine": "rustfs",
|
|
"engine_version": "1.0.0",
|
|
"engine_image": f"registry.internal/rustfs@sha256:{'7' * 64}",
|
|
"client": "rustfs-rc",
|
|
"client_version": "0.1.36",
|
|
"client_image": f"registry.internal/rustfs-rc@sha256:{'8' * 64}",
|
|
"engine_runtime_image": f"registry.internal/rustfs@sha256:{'7' * 64}",
|
|
"engine_resolved_image_id": f"sha256:{'9' * 64}",
|
|
"client_resolved_image_id": f"sha256:{'a' * 64}",
|
|
"bucket": "ai-platform-uploads",
|
|
"path": "minio",
|
|
"object_count": len(minio_records),
|
|
"total_size_bytes": sum(int(item["size_bytes"]) for item in minio_records),
|
|
"content_index_sha256": backup_run._minio_content_index(minio_records),
|
|
},
|
|
"artifacts": artifacts,
|
|
}
|
|
(backup_dir / "manifest.json").write_text(
|
|
json.dumps(manifest),
|
|
encoding="utf-8",
|
|
)
|
|
return backup_dir, manifest
|
|
|
|
|
|
def _restore_check_fixture(source: dict[str, object]) -> list[dict[str, str]]:
|
|
database_names = source["database_names"]
|
|
assert isinstance(database_names, dict)
|
|
return [
|
|
{
|
|
"label": "contract",
|
|
"database": database_names["contract"],
|
|
"restore_database": "yuqei_restore_check_contract_20260927010000",
|
|
"validation": "contract_api_alembic_version",
|
|
"validation_value": "20260926_0081",
|
|
"schema_revision": "20260926_0081",
|
|
},
|
|
{
|
|
"label": "ai_platform",
|
|
"database": database_names["ai_platform"],
|
|
"restore_database": "yuqei_restore_check_ai_platform_20260927010000",
|
|
"validation": "ai_platform_alembic_version",
|
|
"validation_value": "20260925_0042",
|
|
"schema_revision": "20260925_0042",
|
|
},
|
|
{
|
|
"label": "operations",
|
|
"database": database_names["operations"],
|
|
"restore_database": "yuqei_restore_check_operations_20260927010000",
|
|
"validation": "public_schema_fingerprint",
|
|
"validation_value": "a" * 32,
|
|
"schema_revision": "a" * 32,
|
|
},
|
|
]
|
|
|
|
|
|
def test_backup_manifest_and_contract_archive_are_verified(tmp_path: Path) -> None:
|
|
backup_dir, _ = _complete_backup_fixture(tmp_path)
|
|
|
|
generated_manifest = backup_run.verify_manifest(backup_dir)
|
|
manifest, verified = backup_verify.validate_backup(backup_dir)
|
|
|
|
assert generated_manifest["backup_id"] == backup_dir.name
|
|
assert manifest["backup_id"] == backup_dir.name
|
|
assert len(verified) == 5
|
|
|
|
|
|
def test_artifacts_v2_adapter_is_consumed_by_first_release_recovery_planner(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
backup_dir, source = _complete_backup_fixture(tmp_path)
|
|
source, verified = backup_verify.validate_backup(backup_dir)
|
|
restored = _restore_check_fixture(source)
|
|
adapted = backup_verify.build_planner_backup_manifest(
|
|
backup_dir=backup_dir,
|
|
source_manifest=source,
|
|
restore_checks=restored,
|
|
environment_id="isolated-uat-01",
|
|
consistency_set_id="consistency-backup-20260716T120000Z",
|
|
)
|
|
adapted_path = tmp_path / "consistent-backup-manifest.json"
|
|
adapted_path.write_text(json.dumps(adapted), encoding="utf-8")
|
|
record = {
|
|
"path": str(adapted_path.resolve()),
|
|
"sha256": hashlib.sha256(adapted_path.read_bytes()).hexdigest(),
|
|
}
|
|
application_images = {
|
|
"CONTRACT_API_IMAGE": source["images"]["contract_api"],
|
|
"AI_PLATFORM_API_IMAGE": source["images"]["ai_platform_api"],
|
|
"AI_WORKER_IMAGE": source["images"]["ai_worker"],
|
|
}
|
|
object_storage = source["object_storage_mirror"]
|
|
infrastructure_images = {
|
|
"MINIO_IMAGE": object_storage["engine_image"],
|
|
"MINIO_MC_IMAGE": object_storage["client_image"],
|
|
}
|
|
package_binding = {
|
|
"release_id": source["release_id"],
|
|
"compose_project_name": source["compose_project_name"],
|
|
"application_images": {
|
|
key: {"reference": reference, "digest": reference.rsplit("@", 1)[1]}
|
|
for key, reference in application_images.items()
|
|
},
|
|
"infrastructure_images": {
|
|
key: {"reference": reference, "digest": reference.rsplit("@", 1)[1]}
|
|
for key, reference in infrastructure_images.items()
|
|
},
|
|
"offline_image_ids": {
|
|
"CONTRACT_API_IMAGE": f"sha256:{'4' * 64}",
|
|
"AI_PLATFORM_API_IMAGE": f"sha256:{'5' * 64}",
|
|
"AI_WORKER_IMAGE": f"sha256:{'6' * 64}",
|
|
"MINIO_IMAGE": object_storage["engine_resolved_image_id"],
|
|
"MINIO_MC_IMAGE": object_storage["client_resolved_image_id"],
|
|
},
|
|
}
|
|
|
|
validated, digest, _completed = snapshot_restore_planner._validate_backup_components(
|
|
{
|
|
"backup_manifest": record,
|
|
"consistency_mode": "quiesced",
|
|
"databases": list(snapshot_restore_planner.REQUIRED_DATABASES),
|
|
},
|
|
{
|
|
"backup_manifest": record,
|
|
"stores": list(snapshot_restore_planner.REQUIRED_STORAGE_TARGETS),
|
|
},
|
|
package_binding=package_binding,
|
|
environment_id="isolated-uat-01",
|
|
consistency_set_id="consistency-backup-20260716T120000Z",
|
|
require_formal_backup_manifest=True,
|
|
)
|
|
|
|
assert validated == adapted
|
|
assert digest == record["sha256"]
|
|
assert adapted["schema_version"] == backup_verify.PLANNER_BACKUP_MANIFEST_SCHEMA
|
|
assert adapted["source_backup_manifest_sha256"] == hashlib.sha256(
|
|
(backup_dir / "manifest.json").read_bytes()
|
|
).hexdigest()
|
|
assert adapted["application_runtime_images"] == application_images
|
|
assert adapted["object_storage_runtime_images"]["MINIO_IMAGE"] == {
|
|
"candidate_reference": object_storage["engine_image"],
|
|
"runtime_reference": object_storage["engine_runtime_image"],
|
|
"resolved_image_id": object_storage["engine_resolved_image_id"],
|
|
}
|
|
assert adapted["databases"]["ai"]["schema_revision"] == "20260925_0042"
|
|
summary = backup_verify.build_restore_check_summary(
|
|
checked_at=datetime(2026, 9, 27, 1, 0, tzinfo=timezone.utc),
|
|
source_manifest=source,
|
|
verified=verified,
|
|
restored=restored,
|
|
source_manifest_sha256=hashlib.sha256(
|
|
(backup_dir / "manifest.json").read_bytes()
|
|
).hexdigest(),
|
|
planner_manifest_record={
|
|
"schema_version": adapted["schema_version"],
|
|
"path": str(adapted_path.resolve()),
|
|
"sha256": record["sha256"],
|
|
"size_bytes": adapted_path.stat().st_size,
|
|
},
|
|
)
|
|
unsigned_summary = {
|
|
key: value for key, value in summary.items() if key != "summary_payload_sha256"
|
|
}
|
|
assert summary["schema_version"] == backup_verify.RESTORE_CHECK_SUMMARY_SCHEMA
|
|
assert summary["summary_payload_sha256"] == backup_verify.canonical_sha256(
|
|
unsigned_summary
|
|
)
|
|
assert summary["independent_restore_test_claimed"] is False
|
|
|
|
|
|
def test_recovery_adapter_rejects_restore_check_for_another_source_database(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
backup_dir, source = _complete_backup_fixture(tmp_path)
|
|
source, _verified = backup_verify.validate_backup(backup_dir)
|
|
restored = _restore_check_fixture(source)
|
|
restored[0]["database"] = "another_contract_database"
|
|
|
|
with pytest.raises(RuntimeError, match="does not bind the source database"):
|
|
backup_verify.build_planner_backup_manifest(
|
|
backup_dir=backup_dir,
|
|
source_manifest=source,
|
|
restore_checks=restored,
|
|
environment_id="isolated-uat-01",
|
|
consistency_set_id="consistency-backup-20260716T120000Z",
|
|
)
|
|
|
|
|
|
def test_recovery_adapter_rejects_legacy_v1_instead_of_relabeling_it(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
backup_dir, source = _complete_backup_fixture(tmp_path)
|
|
source["schema_version"] = "yuqei-production-backup.v1"
|
|
|
|
with pytest.raises(RuntimeError, match="Only artifacts.v2"):
|
|
backup_verify.build_planner_backup_manifest(
|
|
backup_dir=backup_dir,
|
|
source_manifest=source,
|
|
restore_checks=[],
|
|
environment_id="isolated-uat-01",
|
|
consistency_set_id="consistency-1",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("field", "value", "message"),
|
|
(
|
|
("engine", "minio", "engine or backup client"),
|
|
("client", "minio-mc", "engine or backup client"),
|
|
("engine_image", "rustfs/rustfs:1.0.0", "image identity is not immutable"),
|
|
("client_image", "rustfs/rc:0.1.36", "image identity is not immutable"),
|
|
("engine_version", "1.0.1", "version metadata is invalid"),
|
|
(
|
|
"engine_runtime_image",
|
|
f"registry.internal/other@sha256:{'b' * 64}",
|
|
"running image does not match",
|
|
),
|
|
),
|
|
)
|
|
def test_v2_backup_manifest_rejects_unbound_object_storage_identity(
|
|
tmp_path: Path,
|
|
field: str,
|
|
value: str,
|
|
message: str,
|
|
) -> None:
|
|
backup_dir, manifest = _complete_backup_fixture(tmp_path)
|
|
manifest["object_storage_mirror"][field] = value
|
|
(backup_dir / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with pytest.raises(RuntimeError, match=message):
|
|
backup_run.verify_manifest(backup_dir)
|
|
with pytest.raises(RuntimeError, match=message):
|
|
backup_verify.validate_backup(backup_dir)
|
|
|
|
|
|
def test_object_storage_runtime_identity_is_bound_to_running_and_client_images(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
values = production_values()
|
|
outputs = iter(
|
|
(
|
|
b"a" * 64 + b"\n",
|
|
values["MINIO_IMAGE"].encode("utf-8") + b"\n",
|
|
b"sha256:" + b"b" * 64 + b"\n",
|
|
b"sha256:" + b"b" * 64 + b"\n",
|
|
b"sha256:" + b"c" * 64 + b"\n",
|
|
)
|
|
)
|
|
commands: list[list[str]] = []
|
|
|
|
def fake_run(command, **_kwargs):
|
|
commands.append(command)
|
|
return subprocess.CompletedProcess(command, 0, stdout=next(outputs), stderr=b"")
|
|
|
|
monkeypatch.setattr(backup_run, "_run", fake_run)
|
|
identity = backup_run._inspect_object_storage_runtime(
|
|
env_file=tmp_path / "production.env",
|
|
values=values,
|
|
)
|
|
|
|
assert identity == {
|
|
"engine_runtime_image": values["MINIO_IMAGE"],
|
|
"engine_resolved_image_id": "sha256:" + "b" * 64,
|
|
"client_runtime_image": values["MINIO_MC_IMAGE"],
|
|
"client_resolved_image_id": "sha256:" + "c" * 64,
|
|
}
|
|
assert commands[1][:3] == ["docker", "inspect", "--format"]
|
|
assert commands[2][:3] == ["docker", "inspect", "--format"]
|
|
assert commands[3][:3] == ["docker", "image", "inspect"]
|
|
assert commands[4][:3] == ["docker", "image", "inspect"]
|
|
|
|
|
|
def test_offline_object_storage_runtime_uses_receipt_bound_config_digests(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
values = production_values()
|
|
engine_id = "sha256:" + "d" * 64
|
|
client_id = "sha256:" + "e" * 64
|
|
outputs = iter(
|
|
(
|
|
b"a" * 64 + b"\n",
|
|
engine_id.encode("ascii") + b"\n",
|
|
engine_id.encode("ascii") + b"\n",
|
|
engine_id.encode("ascii") + b"\n",
|
|
client_id.encode("ascii") + b"\n",
|
|
)
|
|
)
|
|
|
|
def fake_run(command, **_kwargs):
|
|
return subprocess.CompletedProcess(command, 0, stdout=next(outputs), stderr=b"")
|
|
|
|
monkeypatch.setattr(backup_run, "_run", fake_run)
|
|
identity = backup_run._inspect_object_storage_runtime(
|
|
env_file=tmp_path / "production.env",
|
|
values=values,
|
|
runtime_image_overrides={
|
|
"MINIO_IMAGE": engine_id,
|
|
"MINIO_MC_IMAGE": client_id,
|
|
},
|
|
)
|
|
|
|
assert identity["engine_runtime_image"] == engine_id
|
|
assert identity["engine_resolved_image_id"] == engine_id
|
|
assert identity["client_runtime_image"] == client_id
|
|
assert identity["client_resolved_image_id"] == client_id
|
|
|
|
|
|
def test_offline_config_digest_without_candidate_bound_receipt_fails_closed(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
values = production_values()
|
|
outputs = iter(
|
|
(
|
|
b"a" * 64 + b"\n",
|
|
("sha256:" + "d" * 64).encode("ascii") + b"\n",
|
|
)
|
|
)
|
|
|
|
def fake_run(command, **_kwargs):
|
|
return subprocess.CompletedProcess(command, 0, stdout=next(outputs), stderr=b"")
|
|
|
|
monkeypatch.setattr(backup_run, "_run", fake_run)
|
|
with pytest.raises(RuntimeError, match="selected runtime identity"):
|
|
backup_run._inspect_object_storage_runtime(
|
|
env_file=tmp_path / "production.env",
|
|
values=values,
|
|
)
|
|
|
|
|
|
def test_release_receipt_binds_offline_ids_to_frozen_candidate_environment(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
values = production_values()
|
|
state = tmp_path / "release-state"
|
|
receipts = state / "receipts"
|
|
receipts.mkdir(parents=True)
|
|
idempotency_key = "install-identity-001"
|
|
snapshot = (
|
|
state
|
|
/ "snapshots"
|
|
/ hashlib.sha256(idempotency_key.encode("utf-8")).hexdigest()
|
|
/ "production.env"
|
|
)
|
|
snapshot.parent.mkdir(parents=True)
|
|
snapshot.write_text(
|
|
"\n".join(
|
|
f"{key}={values[key]}"
|
|
for key in ("RELEASE_ID", "COMPOSE_PROJECT_NAME", *backup_run.RUNTIME_IMAGE_KEYS)
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
compose_files = []
|
|
for relative in (
|
|
"yuqei-infra/compose/docker-compose.yml",
|
|
"yuqei-infra/compose/docker-compose.production.yml",
|
|
"yuqei-infra/compose/docker-compose.production.offline.yml",
|
|
):
|
|
target = snapshot.parent.joinpath(*Path(relative).parts)
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text("services: {}\n", encoding="utf-8")
|
|
compose_files.append(target)
|
|
|
|
def canonical_bytes(payload):
|
|
return json.dumps(
|
|
payload,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
|
|
snapshot_manifest = {
|
|
"schema_version": "yuqei.release-input-snapshot.v2",
|
|
"plan_sha256": "1" * 64,
|
|
"tls_material_binding_sha256": "2" * 64,
|
|
"execution_env_sha256": hashlib.sha256(snapshot.read_bytes()).hexdigest(),
|
|
"files": [
|
|
{
|
|
"path": path.as_posix(),
|
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
"size_bytes": path.stat().st_size,
|
|
}
|
|
for path in (snapshot, *compose_files)
|
|
],
|
|
}
|
|
snapshot_manifest["snapshot_payload_sha256"] = hashlib.sha256(
|
|
canonical_bytes(snapshot_manifest)
|
|
).hexdigest()
|
|
snapshot_manifest_path = snapshot.parent / "snapshot-manifest.json"
|
|
snapshot_manifest_path.write_bytes(canonical_bytes(snapshot_manifest) + b"\n")
|
|
overrides = {
|
|
key: "sha256:" + f"{index:x}"[-1] * 64
|
|
for index, key in enumerate(backup_run.RUNTIME_IMAGE_KEYS, start=1)
|
|
}
|
|
overrides["AI_PLATFORM_RELEASE_IMAGE_DIGEST"] = "sha256:" + "f" * 64
|
|
receipt = {
|
|
"execution_mode": "builtin_fixed_subprocess_runner",
|
|
"status": "backend_verified",
|
|
"result": {"returncode": 0},
|
|
"target_release_id": values["RELEASE_ID"],
|
|
"compose_project_name": values["COMPOSE_PROJECT_NAME"],
|
|
"idempotency_key": idempotency_key,
|
|
"execution_env_sha256": hashlib.sha256(snapshot.read_bytes()).hexdigest(),
|
|
"snapshot_payload_sha256": snapshot_manifest["snapshot_payload_sha256"],
|
|
"environment_overrides": overrides,
|
|
"post_apply_check": {
|
|
"services": [{"service": "minio", "image_id": overrides["MINIO_IMAGE"]}],
|
|
},
|
|
}
|
|
receipt_path = receipts / "receipt.json"
|
|
receipt_path.write_text("{}\n", encoding="utf-8")
|
|
monkeypatch.setattr(
|
|
backup_run,
|
|
"_load_release_package_module",
|
|
lambda: SimpleNamespace(
|
|
_read_existing_receipt=lambda _path: receipt,
|
|
_read_json=lambda path, **_kwargs: json.loads(path.read_text(encoding="utf-8")),
|
|
canonical_json_bytes=canonical_bytes,
|
|
canonical_sha256=lambda payload: hashlib.sha256(canonical_bytes(payload)).hexdigest(),
|
|
),
|
|
)
|
|
|
|
actual_overrides, actual_compose = backup_run._release_runtime_overrides(
|
|
receipt_path=receipt_path,
|
|
values=values,
|
|
)
|
|
assert actual_overrides == overrides
|
|
assert actual_compose == tuple(path.resolve() for path in compose_files)
|
|
|
|
discovered = backup_run._discover_runtime_identity(
|
|
state_root=state,
|
|
values=values,
|
|
)
|
|
assert discovered is not None
|
|
assert discovered[0] == receipt_path
|
|
second_receipt = receipts / "second.json"
|
|
second_receipt.write_text("{}\n", encoding="utf-8")
|
|
with pytest.raises(RuntimeError, match="exactly one fully validated receipt"):
|
|
backup_run._discover_runtime_identity(
|
|
state_root=state,
|
|
values=values,
|
|
)
|
|
second_receipt.unlink()
|
|
|
|
values["MINIO_IMAGE"] = f"registry.internal/other@sha256:{'9' * 64}"
|
|
with pytest.raises(RuntimeError, match="candidate binding differs for MINIO_IMAGE"):
|
|
backup_run._release_runtime_overrides(
|
|
receipt_path=receipt_path,
|
|
values=values,
|
|
)
|
|
|
|
|
|
def test_offline_writer_restore_uses_frozen_three_layer_compose_and_never_pulls(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
compose_files = (
|
|
tmp_path / "docker-compose.yml",
|
|
tmp_path / "docker-compose.production.yml",
|
|
tmp_path / "docker-compose.production.offline.yml",
|
|
)
|
|
calls: list[list[str]] = []
|
|
|
|
def fake_run(command, **_kwargs):
|
|
calls.append(command)
|
|
stdout = b"contract-api\n" if "ps" in command else b""
|
|
return subprocess.CompletedProcess(command, 0, stdout=stdout, stderr=b"")
|
|
|
|
monkeypatch.setattr(backup_run, "_run", fake_run)
|
|
attempts = backup_run._restore_stopped_writers(
|
|
env_file=tmp_path / "production.env",
|
|
project_name="yuqei-v2-production",
|
|
stopped_writers=("contract-api",),
|
|
runtime_env={"MINIO_IMAGE": "sha256:" + "1" * 64},
|
|
compose_files=compose_files,
|
|
)
|
|
|
|
assert attempts[0]["command_succeeded"] is True
|
|
assert attempts[0]["running_verified"] is True
|
|
for command in calls:
|
|
assert command.count("-f") == 3
|
|
assert str(compose_files[2]) in command
|
|
restore_command = calls[0]
|
|
assert restore_command[restore_command.index("--pull") + 1] == "never"
|
|
|
|
|
|
@pytest.mark.parametrize("field", ("backup_id", "created_at", "release_id", "images"))
|
|
def test_v2_backup_manifest_requires_complete_envelope(
|
|
tmp_path: Path,
|
|
field: str,
|
|
) -> None:
|
|
backup_dir, manifest = _complete_backup_fixture(tmp_path)
|
|
manifest.pop(field)
|
|
(backup_dir / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with pytest.raises(RuntimeError, match="manifest fields"):
|
|
backup_run.verify_manifest(backup_dir)
|
|
with pytest.raises(RuntimeError, match="manifest fields"):
|
|
backup_verify.validate_backup(backup_dir)
|
|
|
|
|
|
def test_create_backup_verifies_partial_before_publishing_completed_name(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
values = production_values()
|
|
values.update(
|
|
{
|
|
"BACKUP_HOST_DIR": str(tmp_path),
|
|
"OBJECT_STORAGE_ENGINE": "rustfs",
|
|
"OBJECT_STORAGE_ENGINE_VERSION": "1.0.0",
|
|
"OBJECT_STORAGE_BACKUP_CLIENT": "rustfs-rc",
|
|
"OBJECT_STORAGE_BACKUP_CLIENT_VERSION": "0.1.36",
|
|
}
|
|
)
|
|
|
|
def fake_run(command, **_kwargs):
|
|
return subprocess.CompletedProcess(command, 0, stdout=b"", stderr=b"")
|
|
|
|
def fake_run_to_file(_command, target, **_kwargs):
|
|
target.write_bytes(b"database-dump")
|
|
|
|
def reject_partial(path):
|
|
assert path.name.startswith(".backup-") and path.name.endswith(".partial")
|
|
raise RuntimeError("synthetic manifest rejection")
|
|
|
|
monkeypatch.setattr(backup_run, "_run", fake_run)
|
|
monkeypatch.setattr(backup_run, "_run_to_file", fake_run_to_file)
|
|
monkeypatch.setattr(backup_run, "_verify_contract_archive", lambda _path: None)
|
|
monkeypatch.setattr(
|
|
backup_run,
|
|
"_inspect_object_storage_runtime",
|
|
lambda **_kwargs: {
|
|
"engine_runtime_image": values["MINIO_IMAGE"],
|
|
"engine_resolved_image_id": f"sha256:{'b' * 64}",
|
|
"client_runtime_image": values["MINIO_MC_IMAGE"],
|
|
"client_resolved_image_id": f"sha256:{'c' * 64}",
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
backup_run,
|
|
"_object_storage_mirror_metadata",
|
|
lambda *_args, **_kwargs: {},
|
|
)
|
|
monkeypatch.setattr(backup_run, "verify_manifest", reject_partial)
|
|
|
|
with pytest.raises(RuntimeError, match="synthetic manifest rejection"):
|
|
backup_run.create_backup(
|
|
env_file=tmp_path / "production.env",
|
|
values=values,
|
|
backup_root=tmp_path,
|
|
quiesce_writers=False,
|
|
)
|
|
|
|
assert not list(tmp_path.glob("backup-*"))
|
|
partials = list(tmp_path.glob(".backup-*.partial"))
|
|
assert len(partials) == 1
|
|
assert (partials[0] / "FAILED").is_file()
|
|
|
|
|
|
def test_writer_restore_failure_quarantines_completed_backup_and_attempts_all(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
values = production_values()
|
|
values.update(
|
|
{
|
|
"BACKUP_HOST_DIR": str(tmp_path),
|
|
"OBJECT_STORAGE_ENGINE": "rustfs",
|
|
"OBJECT_STORAGE_ENGINE_VERSION": "1.0.0",
|
|
"OBJECT_STORAGE_BACKUP_CLIENT": "rustfs-rc",
|
|
"OBJECT_STORAGE_BACKUP_CLIENT_VERSION": "0.1.36",
|
|
}
|
|
)
|
|
restored: list[str] = []
|
|
compose_calls: list[list[str]] = []
|
|
ps_calls = 0
|
|
frozen_compose = (
|
|
tmp_path / "docker-compose.yml",
|
|
tmp_path / "docker-compose.production.yml",
|
|
tmp_path / "docker-compose.production.offline.yml",
|
|
)
|
|
|
|
def fake_run(command, **_kwargs):
|
|
nonlocal ps_calls
|
|
if command[:2] == ["docker", "compose"]:
|
|
compose_calls.append(command)
|
|
if "ps" in command and "--status" in command:
|
|
ps_calls += 1
|
|
stdout = ("\n".join(backup_run.WRITER_SERVICES) + "\n").encode("utf-8")
|
|
return subprocess.CompletedProcess(command, 0, stdout=stdout, stderr=b"")
|
|
if "up" in command and "--no-build" in command:
|
|
service = command[-1]
|
|
restored.append(service)
|
|
return subprocess.CompletedProcess(
|
|
command,
|
|
19 if service == backup_run.WRITER_SERVICES[0] else 0,
|
|
stdout=b"",
|
|
stderr=b"synthetic restore failure" if not restored[:-1] else b"",
|
|
)
|
|
return subprocess.CompletedProcess(command, 0, stdout=b"", stderr=b"")
|
|
|
|
def fake_run_to_file(_command, target, **_kwargs):
|
|
compose_calls.append(_command)
|
|
target.write_bytes(b"database-dump")
|
|
|
|
monkeypatch.setattr(
|
|
backup_run,
|
|
"_release_runtime_overrides",
|
|
lambda **_kwargs: (
|
|
{
|
|
**{
|
|
key: "sha256:" + "d" * 64
|
|
for key in backup_run.RUNTIME_IMAGE_KEYS
|
|
},
|
|
"AI_PLATFORM_RELEASE_IMAGE_DIGEST": "sha256:" + "e" * 64,
|
|
},
|
|
frozen_compose,
|
|
),
|
|
)
|
|
monkeypatch.setattr(backup_run, "_run", fake_run)
|
|
monkeypatch.setattr(backup_run, "_run_to_file", fake_run_to_file)
|
|
monkeypatch.setattr(backup_run, "_verify_contract_archive", lambda _path: None)
|
|
monkeypatch.setattr(
|
|
backup_run,
|
|
"_inspect_object_storage_runtime",
|
|
lambda **_kwargs: {
|
|
"engine_runtime_image": values["MINIO_IMAGE"],
|
|
"engine_resolved_image_id": f"sha256:{'b' * 64}",
|
|
"client_runtime_image": values["MINIO_MC_IMAGE"],
|
|
"client_resolved_image_id": f"sha256:{'c' * 64}",
|
|
},
|
|
)
|
|
monkeypatch.setattr(backup_run, "_object_storage_mirror_metadata", lambda *_a, **_k: {})
|
|
monkeypatch.setattr(backup_run, "verify_manifest", lambda _path: {})
|
|
|
|
with pytest.raises(RuntimeError, match="could not be restored and verified"):
|
|
backup_run.create_backup(
|
|
env_file=tmp_path / "production.env",
|
|
values=values,
|
|
backup_root=tmp_path,
|
|
quiesce_writers=True,
|
|
runtime_receipt=tmp_path / "receipt.json",
|
|
)
|
|
|
|
assert restored == list(backup_run.WRITER_SERVICES)
|
|
assert ps_calls == 2
|
|
assert not list(tmp_path.glob("backup-*"))
|
|
partials = list(tmp_path.glob(".backup-*.partial"))
|
|
assert len(partials) == 1
|
|
failure = json.loads((partials[0] / "failure.json").read_text(encoding="utf-8"))
|
|
backup_manifest = json.loads(
|
|
(partials[0] / "manifest.json").read_text(encoding="utf-8")
|
|
)
|
|
assert failure["status"] == "failed"
|
|
assert set(backup_manifest["images"].values()) == {f"sha256:{'d' * 64}"}
|
|
assert failure["writer_restore_attempts"][0]["command_succeeded"] is False
|
|
assert len(failure["writer_restore_attempts"]) == len(backup_run.WRITER_SERVICES)
|
|
assert compose_calls
|
|
for command in compose_calls:
|
|
assert command.count("-f") == 3
|
|
assert str(frozen_compose[2]) in command
|
|
|
|
|
|
def test_sigterm_enters_writer_restore_path_and_cannot_publish_success(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
values = production_values()
|
|
values.update(
|
|
{
|
|
"BACKUP_HOST_DIR": str(tmp_path),
|
|
"OBJECT_STORAGE_ENGINE": "rustfs",
|
|
"OBJECT_STORAGE_ENGINE_VERSION": "1.0.0",
|
|
"OBJECT_STORAGE_BACKUP_CLIENT": "rustfs-rc",
|
|
"OBJECT_STORAGE_BACKUP_CLIENT_VERSION": "0.1.36",
|
|
}
|
|
)
|
|
installed_handlers: dict[int, object] = {}
|
|
restored: list[str] = []
|
|
|
|
def fake_signal(signum, handler):
|
|
if callable(handler):
|
|
installed_handlers[signum] = handler
|
|
|
|
def fake_run(command, **_kwargs):
|
|
if "ps" in command and "--status" in command:
|
|
stdout = ("\n".join(backup_run.WRITER_SERVICES) + "\n").encode("utf-8")
|
|
return subprocess.CompletedProcess(command, 0, stdout=stdout, stderr=b"")
|
|
if "up" in command and "--no-build" in command:
|
|
restored.append(command[-1])
|
|
return subprocess.CompletedProcess(command, 0, stdout=b"", stderr=b"")
|
|
|
|
def interrupting_dump(_command, _target, **_kwargs):
|
|
handler = installed_handlers[signal.SIGTERM]
|
|
assert callable(handler)
|
|
handler(signal.SIGTERM, None)
|
|
|
|
monkeypatch.setattr(backup_run.signal, "signal", fake_signal)
|
|
monkeypatch.setattr(backup_run, "_run", fake_run)
|
|
monkeypatch.setattr(backup_run, "_run_to_file", interrupting_dump)
|
|
|
|
with pytest.raises(backup_run.BackupInterrupted, match="SIGTERM"):
|
|
backup_run.create_backup(
|
|
env_file=tmp_path / "production.env",
|
|
values=values,
|
|
backup_root=tmp_path,
|
|
quiesce_writers=True,
|
|
)
|
|
|
|
assert restored == list(backup_run.WRITER_SERVICES)
|
|
assert not list(tmp_path.glob("backup-*"))
|
|
partials = list(tmp_path.glob(".backup-*.partial"))
|
|
assert len(partials) == 1
|
|
failure = json.loads((partials[0] / "failure.json").read_text(encoding="utf-8"))
|
|
assert failure["signal"] == "SIGTERM"
|
|
assert all(item["running_verified"] for item in failure["writer_restore_attempts"])
|
|
|
|
|
|
def test_backup_manifest_requires_all_databases_contract_files_and_minio(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
backup_dir, manifest = _complete_backup_fixture(tmp_path)
|
|
manifest["artifacts"] = [
|
|
item
|
|
for item in manifest["artifacts"]
|
|
if item["path"] != "postgres-operations.dump"
|
|
]
|
|
(backup_dir / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
with pytest.raises(RuntimeError, match="missing required artifacts"):
|
|
backup_verify.validate_backup(backup_dir)
|
|
|
|
legacy_dir, legacy_manifest = _complete_backup_fixture(tmp_path / "legacy")
|
|
legacy_manifest["schema_version"] = "yuqei-production-backup.v1"
|
|
legacy_manifest.pop("object_storage_mirror")
|
|
legacy_manifest["artifacts"] = [
|
|
item
|
|
for item in legacy_manifest["artifacts"]
|
|
if not str(item["path"]).startswith("minio/")
|
|
]
|
|
shutil.rmtree(legacy_dir / "minio")
|
|
(legacy_dir / "minio").mkdir()
|
|
(legacy_dir / "manifest.json").write_text(
|
|
json.dumps(legacy_manifest), encoding="utf-8"
|
|
)
|
|
with pytest.raises(RuntimeError, match="no MinIO mirror metadata or recorded MinIO content"):
|
|
backup_verify.validate_backup(legacy_dir)
|
|
|
|
|
|
def test_backup_manifest_supports_explicit_empty_minio_and_legacy_content(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
empty_dir, empty_manifest = _complete_backup_fixture(tmp_path / "empty")
|
|
minio_path = empty_dir / "minio" / "tenant-a" / "object.bin"
|
|
minio_path.unlink()
|
|
empty_manifest["artifacts"] = [
|
|
item
|
|
for item in empty_manifest["artifacts"]
|
|
if not str(item["path"]).startswith("minio/")
|
|
]
|
|
empty_manifest["object_storage_mirror"].update({
|
|
"bucket": "ai-platform-uploads",
|
|
"path": "minio",
|
|
"object_count": 0,
|
|
"total_size_bytes": 0,
|
|
"content_index_sha256": backup_run._minio_content_index([]),
|
|
})
|
|
(empty_dir / "manifest.json").write_text(
|
|
json.dumps(empty_manifest), encoding="utf-8"
|
|
)
|
|
backup_verify.validate_backup(empty_dir)
|
|
|
|
legacy_dir, legacy_manifest = _complete_backup_fixture(tmp_path / "legacy-content")
|
|
legacy_manifest["schema_version"] = "yuqei-production-backup.v1"
|
|
legacy_manifest.pop("object_storage_mirror")
|
|
(legacy_dir / "manifest.json").write_text(
|
|
json.dumps(legacy_manifest), encoding="utf-8"
|
|
)
|
|
backup_verify.validate_backup(legacy_dir)
|
|
|
|
|
|
@pytest.mark.parametrize("member_type", (tarfile.SYMTYPE, tarfile.LNKTYPE, tarfile.CHRTYPE))
|
|
def test_contract_archive_rejects_links_and_special_entries(
|
|
tmp_path: Path,
|
|
member_type: bytes,
|
|
) -> None:
|
|
backup_dir, manifest = _complete_backup_fixture(tmp_path)
|
|
archive_path = backup_dir / "contract-files.tar.gz"
|
|
with tarfile.open(archive_path, mode="w:gz") as archive:
|
|
info = tarfile.TarInfo("unsafe-entry")
|
|
info.type = member_type
|
|
if member_type in {tarfile.SYMTYPE, tarfile.LNKTYPE}:
|
|
info.linkname = "outside"
|
|
archive.addfile(info)
|
|
for item in manifest["artifacts"]:
|
|
if item["path"] == "contract-files.tar.gz":
|
|
item["size_bytes"] = archive_path.stat().st_size
|
|
item["sha256"] = hashlib.sha256(archive_path.read_bytes()).hexdigest()
|
|
(backup_dir / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with pytest.raises(RuntimeError, match="link or special entry"):
|
|
backup_run.verify_manifest(backup_dir)
|
|
with pytest.raises(RuntimeError, match="link or special entry"):
|
|
backup_verify.validate_backup(backup_dir)
|
|
|
|
|
|
def test_provider_acceptance_separates_technical_success_from_legal_approval() -> None:
|
|
result = provider_acceptance.evaluate_case(
|
|
row={
|
|
"sample_id": "KBQA-1",
|
|
"expected_citation_required": "true",
|
|
"expected_source_ids": "LAW-CIVIL-001",
|
|
"production_ready": "false",
|
|
"status": "LEGAL_REVIEW",
|
|
"legal_reviewer": "待法务复核",
|
|
},
|
|
suite="legal_qa",
|
|
response={
|
|
"answer": "应结合合同和证据进一步判断。",
|
|
"citations": [{"document_id": "LAW-CIVIL-001"}],
|
|
},
|
|
audit={
|
|
"id": "run-1",
|
|
"status": "succeeded",
|
|
"provider_status": "succeeded",
|
|
"provider": "deepseek",
|
|
"model": "deepseek-chat",
|
|
"quality_labels": [],
|
|
},
|
|
latency_ms=100,
|
|
expected_boundary="不得直接判断个案效力。",
|
|
expected_severity=None,
|
|
)
|
|
|
|
assert result.technical_status == "passed"
|
|
assert result.legal_status == "needs_legal_review"
|
|
assert result.matched_source_ids == ["LAW-CIVIL-001"]
|
|
|
|
|
|
def test_provider_acceptance_rejects_fallback_audit() -> None:
|
|
result = provider_acceptance.evaluate_case(
|
|
row={"sample_id": "KBQA-2", "expected_citation_required": "false"},
|
|
suite="legal_qa",
|
|
response={"answer": "fallback answer", "citations": []},
|
|
audit={
|
|
"id": "run-2",
|
|
"status": "fallback",
|
|
"provider_status": "failed",
|
|
"provider_error": "provider unavailable",
|
|
"quality_labels": [{"code": "fallback_used"}],
|
|
},
|
|
latency_ms=50,
|
|
expected_boundary="",
|
|
expected_severity=None,
|
|
)
|
|
|
|
assert result.technical_status == "failed"
|
|
assert "provider_not_succeeded" in result.problems
|
|
assert "fallback_or_provider_failure_label" in result.problems
|
|
|
|
|
|
def test_provider_acceptance_requires_explicit_stable_source_ids() -> None:
|
|
observed = provider_acceptance._response_source_ids(
|
|
[
|
|
{"document_id": "doc-LAW-CIVIL-001-random"},
|
|
{"document_id": "doc-random", "reference": "LAW-CONTRACT-001 article 1"},
|
|
{"document_id": "doc-random-2", "source_ids": ["LAW-CIVIL-001", "not-stable"]},
|
|
]
|
|
)
|
|
|
|
assert observed == {"LAW-CIVIL-001", "LAW-CONTRACT-001"}
|
|
|
|
|
|
def test_provider_acceptance_loads_phase13_knowledge_cards_idempotently() -> None:
|
|
documents: list[dict[str, object]] = []
|
|
|
|
class Client:
|
|
def get(self, path: str):
|
|
assert path.startswith("/api/v1/knowledge-documents/search?")
|
|
return documents, 1
|
|
|
|
def post(self, path: str, payload: dict[str, object]):
|
|
assert path == "/api/v1/knowledge-documents"
|
|
documents.append({"id": f"doc-{len(documents)}", **payload})
|
|
return documents[-1], 1
|
|
|
|
client = Client()
|
|
|
|
assert provider_acceptance.load_phase13_knowledge_assets(client) == 10
|
|
assert provider_acceptance.load_phase13_knowledge_assets(client) == 0
|
|
assert len(documents) == 10
|
|
assert all(str(item["reference"]).startswith("LAW-") for item in documents)
|
|
assert all("不代表法务批准" in str(item["content"]) for item in documents)
|
|
|
|
|
|
def test_provider_acceptance_requires_complete_named_legal_approval(tmp_path: Path) -> None:
|
|
qa_rows = provider_acceptance.load_rows(provider_acceptance.QA_INVENTORY)
|
|
review_rows = provider_acceptance.load_rows(provider_acceptance.REVIEW_INVENTORY)
|
|
card_rows = provider_acceptance.load_rows(provider_acceptance.KNOWLEDGE_CARD_INVENTORY)
|
|
approval_file = tmp_path / "legal-approval.json"
|
|
approval_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"status": "approved",
|
|
"reviewer": "Named legal reviewer",
|
|
"approved_at": "2026-08-10T08:00:00+08:00",
|
|
"evidence_ref": "https://legal.example.test/approvals/provider-phase13",
|
|
"evidence_sha256": "a" * 64,
|
|
"approved_qa_sample_ids": [row["sample_id"] for row in qa_rows],
|
|
"approved_review_sample_ids": [row["sample_id"] for row in review_rows],
|
|
"approved_knowledge_card_ids": [row["knowledge_card_id"] for row in card_rows],
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
approval = provider_acceptance.load_legal_approval(
|
|
approval_file,
|
|
qa_sample_ids=[row["sample_id"] for row in qa_rows],
|
|
review_sample_ids=[row["sample_id"] for row in review_rows],
|
|
)
|
|
|
|
assert approval["legal_approval_status"] == "approved"
|
|
assert approval["legal_reviewer"] == "Named legal reviewer"
|
|
assert len(approval["approved_knowledge_card_ids"]) == 10
|
|
|
|
tampered = json.loads(approval_file.read_text(encoding="utf-8"))
|
|
tampered["approved_review_sample_ids"] = tampered["approved_review_sample_ids"][:-1]
|
|
approval_file.write_text(json.dumps(tampered), encoding="utf-8")
|
|
with pytest.raises(ValueError, match="all 8 contract review samples"):
|
|
provider_acceptance.load_legal_approval(
|
|
approval_file,
|
|
qa_sample_ids=[row["sample_id"] for row in qa_rows],
|
|
review_sample_ids=[row["sample_id"] for row in review_rows],
|
|
)
|
|
|
|
|
|
def test_provider_acceptance_registers_only_complete_10_plus_8_manifest() -> None:
|
|
qa_rows = provider_acceptance.load_rows(provider_acceptance.QA_INVENTORY)
|
|
review_rows = provider_acceptance.load_rows(provider_acceptance.REVIEW_INVENTORY)
|
|
card_rows = provider_acceptance.load_rows(provider_acceptance.KNOWLEDGE_CARD_INVENTORY)
|
|
cases = []
|
|
for suite, rows in (("legal_qa", qa_rows), ("contract_review", review_rows)):
|
|
for row in rows:
|
|
cases.append(
|
|
provider_acceptance.CaseResult(
|
|
sample_id=row["sample_id"],
|
|
suite=suite,
|
|
technical_status="passed",
|
|
legal_status="needs_legal_review",
|
|
latency_ms=1,
|
|
provider="openai-compatible",
|
|
model="runtime-model",
|
|
audit_run_id=f"run-{row['sample_id']}",
|
|
answer_non_empty=True,
|
|
citation_count=2,
|
|
expected_source_ids=[],
|
|
matched_source_ids=[],
|
|
expected_boundary="",
|
|
expected_severity=None,
|
|
observed_severity=None,
|
|
production_ready=False,
|
|
asset_status="LEGAL_REVIEW",
|
|
legal_reviewer="",
|
|
problems=[],
|
|
)
|
|
)
|
|
|
|
class Client:
|
|
payload: dict[str, object] | None = None
|
|
tenant_id = "default"
|
|
|
|
def post(
|
|
self,
|
|
path: str,
|
|
payload: dict[str, object],
|
|
*,
|
|
extra_headers: dict[str, str] | None = None,
|
|
):
|
|
assert path == "/api/v1/quality/provider-acceptance-runs"
|
|
assert extra_headers
|
|
assert extra_headers["X-AI-Platform-Legal-Attestation"] == payload["legal_approval_attestation"]
|
|
self.payload = payload
|
|
return {"id": "provider-acceptance-complete"}, 1
|
|
|
|
client = Client()
|
|
run_id = provider_acceptance.register_acceptance_manifest(
|
|
client,
|
|
provider_config={
|
|
"id": "provider-config-1",
|
|
"provider": "openai-compatible",
|
|
"model": "runtime-model",
|
|
},
|
|
qa_rows=qa_rows,
|
|
review_rows=review_rows,
|
|
cases=cases,
|
|
approval={
|
|
"legal_approval_status": "approved",
|
|
"legal_reviewer": "Named legal reviewer",
|
|
"legal_approved_at": "2026-08-10T08:00:00+08:00",
|
|
"legal_approval_evidence_ref": "https://legal.example.test/approval",
|
|
"legal_approval_evidence_sha256": "b" * 64,
|
|
"approved_knowledge_card_ids": [row["knowledge_card_id"] for row in card_rows],
|
|
},
|
|
legal_attestation_secret="independent-legal-attestation-secret-for-tests", # pragma: allowlist secret
|
|
)
|
|
|
|
assert run_id == "provider-acceptance-complete"
|
|
assert client.payload is not None
|
|
assert len(client.payload["qa_sample_ids"]) == 10
|
|
assert len(client.payload["review_sample_ids"]) == 8
|
|
assert len(client.payload["knowledge_card_ids"]) == 10
|
|
assert len(client.payload["audit_bindings"]) == 18
|
|
with pytest.raises(ValueError, match="exactly 10 QA and 8 review"):
|
|
provider_acceptance.register_acceptance_manifest(
|
|
client,
|
|
provider_config={"id": "p", "provider": "p", "model": "m"},
|
|
qa_rows=qa_rows[:-1],
|
|
review_rows=review_rows,
|
|
cases=cases[:-1],
|
|
approval={},
|
|
legal_attestation_secret="independent-legal-attestation-secret-for-tests", # pragma: allowlist secret
|
|
)
|
|
|
|
|
|
def _valid_provider_receipt_payload(
|
|
*,
|
|
run_id: str = "provider-acceptance-complete",
|
|
key_id: str = "staging-receipt-key-1",
|
|
target_release_id: str = "v2.0.0-rc.1",
|
|
target_image_digest: str = "sha256:" + "2" * 64,
|
|
) -> dict[str, object]:
|
|
qa_rows = provider_acceptance.load_rows(provider_acceptance.QA_INVENTORY)
|
|
review_rows = provider_acceptance.load_rows(provider_acceptance.REVIEW_INVENTORY)
|
|
card_rows = provider_acceptance.load_rows(provider_acceptance.KNOWLEDGE_CARD_INVENTORY)
|
|
audit_bindings = [
|
|
{
|
|
"suite": suite,
|
|
"sample_id": row["sample_id"],
|
|
"audit_run_id": f"audit-{index}",
|
|
"audit_snapshot_sha256": f"{index:064x}",
|
|
}
|
|
for index, (suite, row) in enumerate(
|
|
[*(('legal_qa', row) for row in qa_rows), *(('contract_review', row) for row in review_rows)],
|
|
start=1,
|
|
)
|
|
]
|
|
return {
|
|
"receipt_version": "provider-acceptance-receipt-v2",
|
|
"algorithm": "Ed25519",
|
|
"key_id": key_id,
|
|
"issuer": "shared_staging",
|
|
"audience": "yuqei-ai-platform-production",
|
|
"source_environment": "shared_staging",
|
|
"target_environment": "production",
|
|
"target_release_id": target_release_id,
|
|
"target_image_digest": target_image_digest,
|
|
"tenant_id": "default",
|
|
"acceptance_run": {
|
|
"source_environment": "shared_staging",
|
|
"acceptance_policy_version": provider_acceptance.ACCEPTANCE_POLICY_VERSION,
|
|
"provider_config_id": "provider-config-1",
|
|
"provider": "openai-compatible",
|
|
"model": "runtime-model",
|
|
"qa_sample_ids": [row["sample_id"] for row in qa_rows],
|
|
"review_sample_ids": [row["sample_id"] for row in review_rows],
|
|
"knowledge_card_ids": [row["knowledge_card_id"] for row in card_rows],
|
|
"audit_bindings": audit_bindings,
|
|
"suite_manifest_sha256": provider_acceptance._manifest_hash(
|
|
[provider_acceptance.QA_INVENTORY, provider_acceptance.REVIEW_INVENTORY]
|
|
),
|
|
"knowledge_manifest_sha256": provider_acceptance._manifest_hash(
|
|
[provider_acceptance.KNOWLEDGE_CARD_INVENTORY]
|
|
),
|
|
"legal_approval_status": "approved",
|
|
"legal_reviewer": "Named legal reviewer",
|
|
"legal_approved_at": "2026-08-10T08:00:00+08:00",
|
|
"legal_approval_evidence_ref": "https://legal.example.test/approval",
|
|
"legal_approval_evidence_sha256": "a" * 64,
|
|
"legal_approval_attestation": "b" * 64,
|
|
"id": run_id,
|
|
"tenant_id": "default",
|
|
"acceptance_manifest_sha256": "c" * 64,
|
|
"created_at": "2026-08-10T00:01:00+00:00",
|
|
},
|
|
"provider_runtime": {
|
|
"provider": "openai-compatible",
|
|
"model": "runtime-model",
|
|
"base_url": "https://provider.example.test/v1",
|
|
"wire_api": "chat_completions",
|
|
"temperature": 0.1,
|
|
"max_tokens": 4096,
|
|
"prompt_templates_sha256": "d" * 64,
|
|
"fingerprint_sha256": "e" * 64,
|
|
},
|
|
"quality_policy_sha256": "f" * 64,
|
|
"issued_at": "2026-08-10T00:02:00+00:00",
|
|
"not_before": "2026-08-10T00:01:30+00:00",
|
|
"expires_at": "2026-08-11T00:02:00+00:00",
|
|
}
|
|
|
|
|
|
def test_provider_acceptance_requires_all_receipt_options_and_legal_approval(tmp_path: Path) -> None:
|
|
values = {
|
|
"receipt_private_key_file": tmp_path / "receipt-key.pem",
|
|
"receipt_key_id": "staging-receipt-key-1",
|
|
"target_release_id": "v2.0.0-rc.1",
|
|
"target_image_digest": "sha256:" + "2" * 64,
|
|
"receipt_output": tmp_path / "receipt.json",
|
|
"legal_approval_file": tmp_path / "legal-approval.json",
|
|
}
|
|
|
|
assert provider_acceptance.validate_receipt_options(SimpleNamespace(**values)) is True
|
|
assert provider_acceptance.validate_receipt_options(
|
|
SimpleNamespace(**{key: None for key in values})
|
|
) is False
|
|
for missing in (
|
|
"receipt_private_key_file",
|
|
"receipt_key_id",
|
|
"target_release_id",
|
|
"target_image_digest",
|
|
"receipt_output",
|
|
):
|
|
with pytest.raises(ValueError, match="all receipt options"):
|
|
provider_acceptance.validate_receipt_options(
|
|
SimpleNamespace(**{**values, missing: None})
|
|
)
|
|
with pytest.raises(ValueError, match="named legal approval"):
|
|
provider_acceptance.validate_receipt_options(
|
|
SimpleNamespace(**{**values, "legal_approval_file": None})
|
|
)
|
|
with pytest.raises(ValueError, match="separate JSON"):
|
|
provider_acceptance.validate_receipt_output_path(
|
|
values["receipt_private_key_file"],
|
|
[values["receipt_private_key_file"]],
|
|
)
|
|
with pytest.raises(ValueError, match="separate JSON"):
|
|
provider_acceptance.validate_receipt_output_path(tmp_path / "receipt.txt", [])
|
|
|
|
|
|
def test_provider_acceptance_fetches_signs_and_writes_separate_receipt(tmp_path: Path) -> None:
|
|
payload = _valid_provider_receipt_payload()
|
|
requested_paths: list[str] = []
|
|
|
|
class Client:
|
|
tenant_id = "default"
|
|
|
|
def get(self, path: str):
|
|
requested_paths.append(path)
|
|
return payload, 1
|
|
|
|
private_key = Ed25519PrivateKey.generate()
|
|
key_file = tmp_path / "receipt-key.pem"
|
|
key_file.write_bytes(
|
|
private_key.private_bytes(
|
|
serialization.Encoding.PEM,
|
|
serialization.PrivateFormat.PKCS8,
|
|
serialization.NoEncryption(),
|
|
)
|
|
)
|
|
receipt_path = tmp_path / "provider-acceptance-receipt.json"
|
|
|
|
metadata = provider_acceptance.create_signed_acceptance_receipt(
|
|
Client(),
|
|
acceptance_run_id="provider-acceptance-complete",
|
|
key_id="staging-receipt-key-1",
|
|
target_release_id="v2.0.0-rc.1",
|
|
target_image_digest="sha256:" + "2" * 64,
|
|
private_key_file=key_file,
|
|
output_path=receipt_path,
|
|
)
|
|
|
|
assert requested_paths == [
|
|
"/api/v1/quality/provider-acceptance-runs/provider-acceptance-complete/receipt-payload"
|
|
"?key_id=staging-receipt-key-1&target_release_id=v2.0.0-rc.1"
|
|
f"&target_image_digest=sha256%3A{'2' * 64}"
|
|
]
|
|
receipt = SignedProviderAcceptanceReceipt.model_validate_json(receipt_path.read_bytes())
|
|
assert receipt.payload == ProviderAcceptanceReceiptPayload.model_validate(payload)
|
|
signature = base64.urlsafe_b64decode(receipt.signature + "==")
|
|
private_key.public_key().verify(
|
|
signature,
|
|
RECEIPT_DOMAIN + canonical_receipt_payload(receipt.payload),
|
|
)
|
|
assert metadata == {
|
|
"receipt_id": receipt.receipt_id,
|
|
"sha256": hashlib.sha256(receipt_path.read_bytes()).hexdigest(),
|
|
}
|
|
provider_acceptance.write_evidence(
|
|
tmp_path,
|
|
provider_acceptance.build_evidence_summary(
|
|
status="passed",
|
|
provider="openai-compatible",
|
|
model="runtime-model",
|
|
cases=[],
|
|
),
|
|
)
|
|
assert receipt_path.exists()
|
|
assert receipt_path.name not in {
|
|
"ai-provider-acceptance-summary.json",
|
|
"ai-provider-acceptance-summary.md",
|
|
}
|
|
|
|
|
|
def test_provider_acceptance_rejects_unsafe_receipt_keys_without_leaking_material(tmp_path: Path) -> None:
|
|
payload = _valid_provider_receipt_payload()
|
|
|
|
class Client:
|
|
tenant_id = "default"
|
|
|
|
def get(self, _path: str):
|
|
return payload, 1
|
|
|
|
rsa_key = rsa.generate_private_key(public_exponent=65537, key_size=2048).private_bytes(
|
|
serialization.Encoding.PEM,
|
|
serialization.PrivateFormat.PKCS8,
|
|
serialization.NoEncryption(),
|
|
)
|
|
key_inputs = {
|
|
"invalid.pem": b"private-secret-marker-not-a-key",
|
|
"rsa.pem": rsa_key,
|
|
"oversized.pem": b"private-secret-marker" * 4096,
|
|
}
|
|
key_paths: list[Path] = [tmp_path / "missing-private-secret-marker.pem"]
|
|
for name, content in key_inputs.items():
|
|
path = tmp_path / name
|
|
path.write_bytes(content)
|
|
key_paths.append(path)
|
|
|
|
for index, key_path in enumerate(key_paths):
|
|
with pytest.raises(ValueError, match="unavailable or invalid") as raised:
|
|
provider_acceptance.create_signed_acceptance_receipt(
|
|
Client(),
|
|
acceptance_run_id="provider-acceptance-complete",
|
|
key_id="staging-receipt-key-1",
|
|
target_release_id="v2.0.0-rc.1",
|
|
target_image_digest="sha256:" + "2" * 64,
|
|
private_key_file=key_path,
|
|
output_path=tmp_path / f"receipt-{index}.json",
|
|
)
|
|
assert "private-secret-marker" not in str(raised.value)
|
|
assert not (tmp_path / f"receipt-{index}.json").exists()
|
|
|
|
|
|
def test_provider_acceptance_never_fetches_or_writes_receipt_without_acceptance_id(tmp_path: Path) -> None:
|
|
class Client:
|
|
tenant_id = "default"
|
|
|
|
def get(self, _path: str):
|
|
raise AssertionError("receipt payload must not be fetched")
|
|
|
|
receipt_path = tmp_path / "receipt.json"
|
|
with pytest.raises(ValueError, match="acceptance_run_id"):
|
|
provider_acceptance.create_signed_acceptance_receipt(
|
|
Client(),
|
|
acceptance_run_id="",
|
|
key_id="staging-receipt-key-1",
|
|
target_release_id="v2.0.0-rc.1",
|
|
target_image_digest="sha256:" + "2" * 64,
|
|
private_key_file=tmp_path / "missing.pem",
|
|
output_path=receipt_path,
|
|
)
|
|
assert not receipt_path.exists()
|
|
|
|
|
|
def test_provider_acceptance_excludes_deterministic_product_suites() -> None:
|
|
assert not hasattr(provider_acceptance, "GENERATION_INVENTORY")
|
|
assert not hasattr(provider_acceptance, "run_generation_case")
|
|
|
|
|
|
def test_contract_product_golden_runs_real_deterministic_product_paths() -> None:
|
|
results = contract_product_golden.run_product_golden()
|
|
summary = contract_product_golden.build_summary(results)
|
|
|
|
assert len(results) == 31
|
|
assert sum(item.suite == "contract_generation" for item in results) == 23
|
|
assert sum(item.suite == "clause_recommendation" for item in results) == 8
|
|
assert all(item.technical_status == "passed" for item in results)
|
|
assert all(item.legal_status == "needs_legal_review" for item in results)
|
|
assert summary["status"] == "needs_legal_review"
|
|
assert summary["counts"]["technical_passed"] == 31
|
|
assert summary["counts"]["legal_approved"] == 0
|
|
assert summary["counts"]["preserved_required_placeholders"] == 0
|
|
assert summary["problems"] == []
|
|
|
|
|
|
def test_contract_product_golden_rejects_inconsistent_expected_boundary() -> None:
|
|
row = contract_product_golden.load_rows()[0]
|
|
tampered = dict(
|
|
row,
|
|
expected_source_citations=row["expected_source_citations"] + ";LAW-NOT-IN-BOUNDARY-001",
|
|
)
|
|
|
|
result = contract_product_golden.evaluate_product_case(tampered)
|
|
|
|
assert result.technical_status == "failed"
|
|
assert "expected_boundary_missing_identifier" in result.problems
|
|
|
|
|
|
def test_contract_product_golden_does_not_approve_names_without_evidence() -> None:
|
|
row = contract_product_golden.load_rows()[0]
|
|
incomplete_approval = dict(
|
|
row,
|
|
status="APPROVED",
|
|
production_ready="true",
|
|
legal_reviewer="Named legal reviewer",
|
|
business_reviewer="Named business reviewer",
|
|
)
|
|
|
|
result = contract_product_golden.evaluate_product_case(incomplete_approval)
|
|
|
|
assert result.technical_status == "passed"
|
|
assert result.legal_status == "needs_legal_review"
|
|
|
|
|
|
def test_contract_product_golden_evidence_does_not_claim_legal_approval(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
summary = contract_product_golden.build_summary(contract_product_golden.run_product_golden())
|
|
|
|
contract_product_golden.write_evidence(tmp_path, summary)
|
|
|
|
assert summary["status"] == "needs_legal_review"
|
|
assert "acceptance_run_id" not in summary
|
|
assert set(path.name for path in tmp_path.iterdir()) == {
|
|
"contract-product-golden-summary.json",
|
|
"contract-product-golden-summary.md",
|
|
}
|
|
|
|
|
|
def test_provider_acceptance_evidence_excludes_responses_and_identifiers(tmp_path: Path) -> None:
|
|
result = provider_acceptance.evaluate_case(
|
|
row={
|
|
"sample_id": "sensitive-sample-id",
|
|
"expected_citation_required": "true",
|
|
"expected_source_ids": "LAW-SECRET-001",
|
|
"production_ready": "false",
|
|
},
|
|
suite="legal_qa",
|
|
response={
|
|
"answer": "sensitive-provider-answer",
|
|
"citations": [{"document_id": "LAW-SECRET-001"}],
|
|
},
|
|
audit={
|
|
"id": "sensitive-audit-run-id",
|
|
"status": "succeeded",
|
|
"provider_status": "succeeded",
|
|
"provider": "deepseek",
|
|
"model": "deepseek-chat",
|
|
"quality_labels": [],
|
|
},
|
|
latency_ms=100,
|
|
expected_boundary="sensitive-legal-boundary",
|
|
expected_severity=None,
|
|
)
|
|
|
|
summary = provider_acceptance.build_evidence_summary(
|
|
status="needs_legal_review",
|
|
provider="deepseek",
|
|
model="deepseek-chat",
|
|
cases=[result],
|
|
)
|
|
provider_acceptance.write_evidence(tmp_path, summary)
|
|
|
|
assert set(summary) == {"provider", "model", "status", "counts", "latency_ms", "problems"}
|
|
serialized = json.dumps(summary, ensure_ascii=False)
|
|
for sensitive_value in (
|
|
"sensitive-sample-id",
|
|
"sensitive-provider-answer",
|
|
"LAW-SECRET-001",
|
|
"sensitive-audit-run-id",
|
|
"sensitive-legal-boundary",
|
|
):
|
|
assert sensitive_value not in serialized
|
|
assert not (tmp_path / "ai-provider-acceptance-responses.json").exists()
|
|
assert set(path.name for path in tmp_path.iterdir()) == {
|
|
"ai-provider-acceptance-summary.json",
|
|
"ai-provider-acceptance-summary.md",
|
|
}
|
|
|
|
|
|
def test_provider_acceptance_validates_safe_runtime_parameters() -> None:
|
|
valid_key = "service-key-that-is-long-enough-for-testing" # pragma: allowlist secret
|
|
|
|
with pytest.raises(ValueError, match="query/fragment"):
|
|
provider_acceptance.ApiClient(
|
|
"https://api.example.test/ai?key=unsafe",
|
|
service_key=valid_key,
|
|
tenant_id="default",
|
|
timeout_seconds=30,
|
|
)
|
|
with pytest.raises(ValueError, match="service key"):
|
|
provider_acceptance.ApiClient(
|
|
"https://api.example.test/ai",
|
|
service_key=f"{valid_key}\r\nInjected: true",
|
|
tenant_id="default",
|
|
timeout_seconds=30,
|
|
)
|
|
with pytest.raises(ValueError, match="Tenant ID"):
|
|
provider_acceptance.ApiClient(
|
|
"https://api.example.test/ai",
|
|
service_key=valid_key,
|
|
tenant_id="default\r\nInjected: true",
|
|
timeout_seconds=30,
|
|
)
|
|
with pytest.raises(ValueError, match="Timeout"):
|
|
provider_acceptance.ApiClient(
|
|
"https://api.example.test/ai",
|
|
service_key=valid_key,
|
|
tenant_id="default",
|
|
timeout_seconds=0,
|
|
)
|
|
|
|
|
|
def test_provider_acceptance_allows_explicit_isolated_memory_schema_warning() -> None:
|
|
class Client:
|
|
def get(self, path: str):
|
|
if path == "/api/v1/runtime/schema-check":
|
|
return {
|
|
"status": "warning",
|
|
"checks": [
|
|
{
|
|
"name": "repository_backend",
|
|
"status": "warning",
|
|
"details": {"repository_backend": "memory"},
|
|
},
|
|
{"name": "anti_abuse_runtime", "status": "warning", "details": {}},
|
|
],
|
|
}, 1
|
|
if path == "/api/v1/model-configs":
|
|
return [
|
|
{
|
|
"provider": "isolated-provider",
|
|
"model": "isolated-model",
|
|
"enabled": True,
|
|
"is_default": True,
|
|
"base_url": "https://provider.example.test",
|
|
"api_key_set": True,
|
|
}
|
|
], 1
|
|
raise AssertionError(path)
|
|
|
|
schema, provider_config = provider_acceptance.validate_runtime(Client())
|
|
|
|
assert schema["status"] == "warning"
|
|
assert provider_config["provider"] == "isolated-provider"
|
|
|
|
|
|
def test_provider_acceptance_allows_local_codex_development_evidence_only() -> None:
|
|
class Client:
|
|
def get(self, path: str):
|
|
if path == "/api/v1/runtime/schema-check":
|
|
return {
|
|
"status": "warning",
|
|
"checks": [
|
|
{
|
|
"name": "repository_backend",
|
|
"status": "warning",
|
|
"details": {"repository_backend": "memory"},
|
|
},
|
|
{"name": "anti_abuse_runtime", "status": "warning", "details": {}},
|
|
{
|
|
"name": "provider_runtime",
|
|
"status": "ok",
|
|
"details": {
|
|
"environment": "local",
|
|
"provider_call_mode": "codex_cli",
|
|
},
|
|
},
|
|
],
|
|
}, 1
|
|
if path == "/api/v1/model-configs":
|
|
return [
|
|
{
|
|
"id": "legacy-development-provider",
|
|
"provider": "legacy-development-config",
|
|
"model": "gpt-pro-development",
|
|
"enabled": True,
|
|
"is_default": True,
|
|
}
|
|
], 1
|
|
raise AssertionError(path)
|
|
|
|
schema, provider_config = provider_acceptance.validate_runtime(
|
|
Client(),
|
|
development_codex_cli=True,
|
|
)
|
|
|
|
assert schema["status"] == "warning"
|
|
assert provider_config["provider"] == "codex-cli-development"
|
|
assert provider_config["model"] == "gpt-5.6-sol"
|
|
assert provider_config["development_runtime_override"] is True
|
|
|
|
|
|
def test_provider_acceptance_never_promotes_development_codex_evidence() -> None:
|
|
with pytest.raises(RuntimeError, match="cannot be treated as shared-staging"):
|
|
provider_acceptance.validate_runtime(
|
|
object(),
|
|
require_shared_staging=True,
|
|
development_codex_cli=True,
|
|
)
|
|
|
|
summary = provider_acceptance.mark_development_evidence(
|
|
provider_acceptance.build_evidence_summary(
|
|
status="needs_legal_review",
|
|
provider=None,
|
|
model=None,
|
|
cases=[],
|
|
)
|
|
)
|
|
|
|
assert summary["provider"] == "codex-cli-development"
|
|
assert summary["model"] == "gpt-5.6-sol"
|
|
assert summary["evidence_scope"] == "local_development_codex_cli"
|
|
assert summary["production_eligible"] is False
|
|
assert summary["provider_acceptance_registration_allowed"] is False
|
|
assert summary["controlled_pilot_gate_effect"] == "none"
|
|
|
|
|
|
def test_provider_acceptance_reports_operational_quality_and_latency_separately() -> None:
|
|
result = provider_acceptance.evaluate_case(
|
|
row={
|
|
"sample_id": "QA-DEVELOPMENT-SLOW-001",
|
|
"expected_citation_required": "false",
|
|
"production_ready": "false",
|
|
"status": "LEGAL_REVIEW",
|
|
"legal_reviewer": "待法务复核",
|
|
},
|
|
suite="legal_qa",
|
|
response={"answer": "有内容的技术回答", "citations": []},
|
|
audit={
|
|
"id": "run-development-slow",
|
|
"status": "succeeded",
|
|
"provider_status": "succeeded",
|
|
"provider": "codex-cli-development",
|
|
"model": "gpt-5.6-sol",
|
|
"quality_status": "failed",
|
|
"quality_score": 60,
|
|
"quality_labels": [{"code": "latency_critical"}],
|
|
},
|
|
latency_ms=128_000,
|
|
expected_boundary="",
|
|
expected_severity=None,
|
|
)
|
|
|
|
summary = provider_acceptance.build_evidence_summary(
|
|
status="needs_operational_review",
|
|
provider=result.provider,
|
|
model=result.model,
|
|
cases=[result],
|
|
)
|
|
|
|
assert result.technical_status == "passed"
|
|
assert result.quality_status == "failed"
|
|
assert result.quality_score == 60
|
|
assert summary["counts"]["technical_passed"] == 1
|
|
assert summary["counts"]["quality_failed"] == 1
|
|
assert summary["latency_ms"] == {
|
|
"min": 128_000,
|
|
"median": 128_000,
|
|
"p95": 128_000,
|
|
"max": 128_000,
|
|
}
|
|
|
|
|
|
def test_provider_acceptance_encodes_chinese_request_as_utf8(monkeypatch) -> None:
|
|
captured: dict[str, object] = {}
|
|
|
|
class Response:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return False
|
|
|
|
def read(self, _limit: int) -> bytes:
|
|
return b'{"status":"ok"}'
|
|
|
|
def fake_urlopen(request, *, timeout):
|
|
captured["body"] = request.data
|
|
captured["timeout"] = timeout
|
|
return Response()
|
|
|
|
monkeypatch.setattr(provider_acceptance, "urlopen", fake_urlopen)
|
|
client = provider_acceptance.ApiClient(
|
|
"https://api.example.test/ai",
|
|
service_key="service-key-that-is-long-enough-for-testing", # pragma: allowlist secret
|
|
tenant_id="default",
|
|
timeout_seconds=30,
|
|
)
|
|
|
|
payload, _ = client.post("/api/v1/legal/qa", {"question": "电子邮件能否作为合同证据?"})
|
|
|
|
assert payload == {"status": "ok"}
|
|
assert json.loads(captured["body"].decode("utf-8"))["question"] == "电子邮件能否作为合同证据?"
|
|
assert captured["timeout"] == 30
|
|
|
|
|
|
def test_provider_acceptance_runs_all_phase13_cases_against_isolated_http_service() -> None:
|
|
qa_rows = provider_acceptance.load_rows(provider_acceptance.QA_INVENTORY)
|
|
review_rows = provider_acceptance.load_rows(provider_acceptance.REVIEW_INVENTORY)
|
|
qa_by_question = {row["question"]: row for row in qa_rows}
|
|
review_by_id = {row["sample_id"]: row for row in review_rows}
|
|
audits: dict[str, dict[str, object]] = {}
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def send_json(self, payload: object) -> None:
|
|
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def do_GET(self) -> None: # noqa: N802
|
|
path = self.path.split("?", 1)[0]
|
|
if path == "/api/v1/runtime/schema-check":
|
|
self.send_json(
|
|
{
|
|
"status": "ok",
|
|
"checks": [{"name": "provider_runtime", "status": "ok"}],
|
|
}
|
|
)
|
|
return
|
|
if path == "/api/v1/model-configs":
|
|
self.send_json(
|
|
[
|
|
{
|
|
"id": "provider-isolated",
|
|
"provider": "isolated-provider",
|
|
"model": "isolated-model",
|
|
"enabled": True,
|
|
"is_default": True,
|
|
"base_url": "https://provider.invalid/v1",
|
|
"api_key_set": True,
|
|
}
|
|
]
|
|
)
|
|
return
|
|
if path == "/api/v1/audit/ai-runs":
|
|
self.send_json(list(reversed(list(audits.values()))))
|
|
return
|
|
run_id = path.removeprefix("/api/v1/audit/ai-runs/")
|
|
if run_id in audits:
|
|
self.send_json(audits[run_id])
|
|
return
|
|
self.send_error(404)
|
|
|
|
def do_POST(self) -> None: # noqa: N802
|
|
content_length = int(self.headers.get("Content-Length", "0"))
|
|
payload = json.loads(self.rfile.read(content_length).decode("utf-8"))
|
|
run_id = f"isolated-run-{len(audits) + 1}"
|
|
if self.path == "/api/v1/legal/qa":
|
|
row = qa_by_question[payload["question"]]
|
|
citations = [
|
|
{"document_id": f"doc-{index}", "source_ids": [source_id]}
|
|
for index, source_id in enumerate(provider_acceptance.split_ids(row["expected_source_ids"]))
|
|
]
|
|
operation = "legal_qa"
|
|
response = {"answer": "基于检索依据给出边界明确的答复。", "citations": citations}
|
|
elif self.path == "/api/v1/legal/contract-review":
|
|
row = review_by_id[payload["contract_title"]]
|
|
citations = [
|
|
{"document_id": f"doc-{index}", "source_ids": [source_id]}
|
|
for index, source_id in enumerate(provider_acceptance.split_ids(row["expected_source_citations"]))
|
|
]
|
|
cited_source_ids = provider_acceptance.split_ids(row["expected_source_citations"])
|
|
operation = "contract_review"
|
|
response = {
|
|
"summary": "已按约定审查重点完成审查。",
|
|
"citations": citations,
|
|
"risk_items": [
|
|
{
|
|
"severity": row["expected_severity"],
|
|
"citation_source_ids": cited_source_ids,
|
|
}
|
|
],
|
|
"audit_run_id": run_id,
|
|
}
|
|
else:
|
|
self.send_error(404)
|
|
return
|
|
audits[run_id] = {
|
|
"id": run_id,
|
|
"operation": operation,
|
|
"status": "succeeded",
|
|
"provider_status": "succeeded",
|
|
"provider": "isolated-provider",
|
|
"model": "isolated-model",
|
|
"quality_labels": [],
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
self.send_json(response)
|
|
|
|
def log_message(self, _format: str, *_args: object) -> None:
|
|
return
|
|
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
thread = Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = provider_acceptance.ApiClient(
|
|
f"http://127.0.0.1:{server.server_port}",
|
|
service_key="isolated-non-secret-service-key-for-tests", # pragma: allowlist secret
|
|
tenant_id="default",
|
|
timeout_seconds=10,
|
|
)
|
|
schema, provider_config = provider_acceptance.validate_runtime(client)
|
|
seen_run_ids: set[str] = set()
|
|
results = [
|
|
provider_acceptance.run_qa_case(client, row, seen_run_ids)[0]
|
|
for row in qa_rows
|
|
]
|
|
results.extend(
|
|
provider_acceptance.run_review_case(client, row, seen_run_ids)[0]
|
|
for row in review_rows
|
|
)
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join(timeout=5)
|
|
|
|
assert len(qa_rows) == 10
|
|
assert len(review_rows) == 8
|
|
assert len(results) == 18
|
|
assert schema["status"] == "ok"
|
|
assert provider_config["provider"] == "isolated-provider"
|
|
assert all(result.technical_status == "passed" for result in results)
|
|
assert all(result.legal_status == "needs_legal_review" for result in results)
|
|
|
|
|
|
def test_production_tool_bandit_suppressions_are_exactly_allowlisted() -> None:
|
|
expected = {
|
|
"yuqei-infra/backup/run-production-backup.py": ["B404", "B603"],
|
|
"yuqei-infra/backup/verify-production-backup.py": ["B404", "B603", "B603"],
|
|
"yuqei-infra/observability/check-production-health.py": ["B310", "B310"],
|
|
"yuqei-infra/scripts/run-ai-provider-acceptance.py": ["B310"],
|
|
}
|
|
observed: dict[str, list[str]] = {}
|
|
for relative_path in expected:
|
|
lines = (REPO_ROOT / relative_path).read_text(encoding="utf-8").splitlines()
|
|
observed[relative_path] = [
|
|
line.split("# nosec", 1)[1].strip()
|
|
for line in lines
|
|
if "# nosec" in line
|
|
]
|
|
|
|
assert observed == expected
|