"""具体规则改进通过真实CLI验证,再由默认HTTP作者审阅并提交owner决定。""" import json import os import socket import subprocess import time from dataclasses import asdict, replace from datetime import UTC, datetime, timedelta from pathlib import Path from threading import Thread from uuid import uuid4 import httpx import pytest import test_规则启用接入 as 接入测试 import test_规则改进与回执 as 改进测试 import test_规则诊断资格与消费 as 规则测试 import uvicorn from fastapi.testclient import TestClient from muse.共享.调用身份 import 用途 from muse.接入.http.应用 import 创建应用 from muse.配置 import 读取配置 pytestmark = pytest.mark.数据库 规则资格环境 = 规则测试.规则资格环境 def _规范时刻(value): # CLI与HTTP保留同一数据库时刻,但JSON分别使用空格和T分隔。 if isinstance(value, dict): return {key: _规范时刻(item) for key, item in value.items()} if isinstance(value, list): return [_规范时刻(item) for item in value] if isinstance(value, str): try: return datetime.fromisoformat(value) except ValueError: pass return value @pytest.mark.case_id( "NC-w26-26d104", environment="隔离PG/合成HTTP及浏览器", given="固定请求、来源与作者;按用例环境准备独立测试输入", when="通过实际公开入口执行并核对拒绝、状态及持久结果", then=["规则改进CLI验证与HTTP作者启停读回"], contract="docs/系统架构/新版设计/模块设计/B07-作者经验.md#有来源的运行观察", ) def test_规则改进CLI验证与HTTP作者启停读回__26d104(规则资格环境, tmp_path): env = 规则资格环境 service, seed, _ = 改进测试._规则改进(env) proposal = replace(seed, improvement_id=str(uuid4())) configs = 接入测试._配置(env, tmp_path) published = 规则测试._publish(env, "rule-improvement-entry", "owned") with TestClient( 创建应用(读取配置(configs[用途.生产])), headers={"Origin": "http://testserver"} ) as client: 接入测试._登录(client) target = client.get("/api/v1/rules/l002/versions/1/target") assert target.status_code == 200, target.text assert target.json()["target"]["rule_hash"] == proposal.rule_hash created = client.post("/api/v1/rule-improvements", json={"proposal": asdict(proposal)}) assert created.status_code == 201, created.text assert "method_version_id" not in created.json()["payload"] point = { "improvement_id": proposal.improvement_id, "revision": created.json()["revision"], "content_hash": created.json()["content_hash"], } path = "/api/v1/improvements/" + proposal.improvement_id assert client.get(path).json()["actions"] == [] assert client.get("/api/v1/rules/l002/activation").json()["current"] is None request = { "command_id": "rule-improvement-entry-validation", "selection": point, "request": { "dataset_version_id": published["version_id"], "rule_id": proposal.rule_id, "rule_version": proposal.rule_version, "rule_hash": proposal.rule_hash, }, } request_file = tmp_path / "规则验证请求.json" request_file.write_text(json.dumps(request, ensure_ascii=False)) validated = 接入测试._cli( tmp_path, configs[用途.生产], "经验", "申请规则验证", request_file, "--评测配置", configs[用途.评测], ) assert set(validated["receipt"]) == {"run_id", "target", "result_hash"} export_file = tmp_path / "规则凭据导出.json" export_file.write_text( json.dumps( { "run_id": validated["receipt"]["run_id"], "approval_ref": "fixture-rule-entry", "valid_until": (datetime.now(UTC) + timedelta(hours=1)).isoformat(), } ) ) proof = 接入测试._cli(tmp_path, configs[用途.维护], "评测", "导出规则凭据", export_file) change = { "rule_id": "l002", "version": 1, "action": "enable", "expected_activation_revision": 0, "credential_id": proof["receipt_id"], } opened = client.post("/api/v1/rules/l002/activation/review", json={"request": change}) assert opened.status_code == 200, opened.text assert client.get("/api/v1/rules/l002/activation").json()["current"] is None decision = { "command_id": "rule-improvement-entry-enable", "selection": point, "request": { **{k: v for k, v in change.items() if k != "version"}, "rule_version": 1, "rule_hash": proposal.rule_hash, "review_id": opened.json()["review_id"], "review_hash": opened.json()["review_hash"], }, } missing_review = {**decision, "request": {**decision["request"], "review_id": ""}} assert client.post(path + "/rule-state", json=missing_review).status_code == 422 wrong_path = client.post( "/api/v1/improvements/" + str(uuid4()) + "/rule-state", json=decision ) assert wrong_path.status_code == 403 assert client.get("/api/v1/rules/l002/activation").json()["current"] is None enabled = client.post(path + "/rule-state", json=decision) assert enabled.status_code == 200, enabled.text assert client.post(path + "/rule-state", json=decision).json() == enabled.json() detail = client.get(path).json() assert detail["target_state"] == "active" and detail["consumption"] == [] diagnosis = client.post("/api/v1/diagnoses", json={"target": env["target"]}) assert diagnosis.status_code == 201, diagnosis.text detail = client.get(path).json() assert len(detail["consumption"]) == 1 assert detail["consumption"][0]["diagnosis_id"] == diagnosis.json()["diagnosis_id"] change.update(action="disable", credential_id="", expected_activation_revision=1) opened = client.post("/api/v1/rules/l002/activation/review", json={"request": change}) assert opened.status_code == 200, opened.text decision.update(command_id="rule-improvement-entry-disable") decision["request"].update( action="disable", credential_id="", expected_activation_revision=1, review_id=opened.json()["review_id"], review_hash=opened.json()["review_hash"], ) decision_file = tmp_path / "规则停用请求.json" decision_file.write_text(json.dumps(decision)) disabled = 接入测试._cli(tmp_path, configs[用途.生产], "经验", "规则启停", decision_file) assert disabled["receipt"]["results"][0]["state"] == "disabled" restored = 接入测试._cli( tmp_path, configs[用途.生产], "经验", "读取改进", proposal.improvement_id ) assert _规范时刻(restored) == _规范时刻(client.get(path).json()) assert restored["target_state"] == "disabled" and len(restored["consumption"]) == 1 assert len(restored["actions"]) == 3 assert service.读取改进(env["author"], proposal.improvement_id)["target_state"] == "disabled" assert env["app"].要求正文().读取正文(env["author"], "rule-ch")["revision"] == 1 @pytest.mark.case_id( "NC-w26-26d105", environment="隔离PG/合成HTTP及浏览器", given="固定请求、来源与作者;按用例环境准备独立测试输入", when="通过实际公开入口执行并核对拒绝、状态及持久结果", then=["规则改进浏览器提案验证与启停消费"], contract="docs/系统架构/新版设计/模块设计/B07-作者经验.md#有来源的运行观察", ) @pytest.mark.浏览器 def test_规则改进浏览器提案验证与启停消费__26d105(规则资格环境, tmp_path): if os.environ.get("MUSE_RUN_BROWSER") != "1": pytest.skip("需显式启用浏览器") env = 规则资格环境 _, seed, _ = 改进测试._规则改进(env) configs = 接入测试._配置(env, tmp_path) published = 规则测试._publish(env, "rule-improvement-browser", "owned") with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] url = f"http://127.0.0.1:{port}" config = 读取配置(configs[用途.生产]) assert config.HTTP is not None config = replace(config, HTTP=replace(config.HTTP, 公开地址=url, 允许来源=(url,))) server = uvicorn.Server( uvicorn.Config(创建应用(config), host="127.0.0.1", port=port, log_level="warning") ) thread = Thread(target=server.run, daemon=True) thread.start() output = Path(os.environ.get("MUSE_BROWSER_ARTIFACTS", str(tmp_path / "browser"))).resolve() output.mkdir(parents=True) try: for _ in range(100): if server.started: break time.sleep(0.05) assert server.started and httpx.get(url + "/ready", trust_env=False).status_code == 200 environment = { **os.environ, "MUSE_WORKBENCH_URL": url, "MUSE_AUTHOR_PASSWORD_FILE": config.HTTP.口令文件, "MUSE_FEEDBACK_ID": seed.feedback_ids[0], "MUSE_RULE_DATASET_ID": published["version_id"], "MUSE_RULE_BODY_TARGET": json.dumps(env["target"]), "MUSE_TEST_PYTHON": os.sys.executable, "MUSE_PRODUCTION_CONFIG": str(configs[用途.生产]), "MUSE_EVALUATION_CONFIG": str(configs[用途.评测]), "MUSE_MAINTENANCE_CONFIG": str(configs[用途.维护]), "MUSE_BROWSER_EXECUTABLE": ( "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ), } with (output / "浏览器.json").open("w") as report: process = subprocess.run( [ "pnpm", "exec", "playwright", "test", "规则改进.spec.ts", "--reporter=json", "--output", str(output / "产物"), ], cwd=Path(__file__).resolve().parents[2] / "web", env=environment, stdout=report, stderr=subprocess.STDOUT, timeout=120, ) assert process.returncode == 0, "浏览器未通过,见浏览器.json" state = env["review"].读取规则状态(env["author"], "l002") assert state["current"]["state"] == "disabled" assert [row["action"] for row in state["history"]] == ["enable", "disable"] assert env["app"].要求正文().读取正文(env["author"], "rule-ch")["revision"] == 1 finally: server.should_exit = True thread.join(10) assert not thread.is_alive()