"""计划与真实发布区分,反馈显式关联,旧记录不随修订改变。""" from dataclasses import asdict, replace from datetime import UTC, datetime, timedelta from uuid import uuid4 import pytest import test_定稿与导出 as 定稿测试 import test_生产评测权限隔离 as 接入测试 from fastapi.testclient import TestClient from muse.交付连载.接口 import 发布请求, 读者反馈请求, 连载章, 连载请求 from muse.共享.错误 import Muse错误 from muse.接入.http.应用 import 创建应用 from muse.配置 import 读取配置 pytestmark = pytest.mark.数据库 候选环境 = 定稿测试.候选环境 交付环境 = 定稿测试.交付环境 时间 = datetime(2026, 9, 1, 12, tzinfo=UTC) def _发布(req, **changes): return replace( 发布请求( str(uuid4()), req.delivery_id, (req.chapters[0].chapter_id,), 时间, "合成平台", "https://example.invalid/release/1", ), **changes, ) @pytest.mark.case_id( "NC-w27-27b001", environment="隔离PG", given="本作者完整目录、确切正文版本与独立隔离数据", when="通过定稿、S02导出、跨章候选或连载反馈实际入口执行并检查失败恢复", then=["计划日期不充当发布且前置章需要实际证据"], contract="docs/系统架构/新版设计/模块设计/B08-交付连载.md", ) def test_计划日期不充当发布且前置章需要实际证据__27b001(交付环境): app, author, _, req = 交付环境 svc = app.要求交付() svc.冻结定稿(author, "freeze", req) a, b = [x.chapter_id for x in req.chapters] plan = 连载请求( str(uuid4()), req.delivery_id, 0, ( 连载章(a, 时间), 连载章(b, 时间 + timedelta(days=1), (a,)), ), ) saved = svc.保存连载(author, "plan", plan) assert svc.保存连载(author, "plan", plan) == saved face = svc.连载工作面(author, req.work_id) assert face["releases"] == [] assert [c["state"] for c in face["plans"][0]["chapters"]] == ["ready", "ready"] with pytest.raises(Muse错误, match="前置"): svc.登记发布( author, "too-early", _发布( req, chapter_ids=(b,), plan_id=plan.plan_id, plan_revision=1, ), ) first = _发布(req, plan_id=plan.plan_id, plan_revision=1) svc.登记发布(author, "release-one", first) svc.登记发布( author, "release-two", _发布( req, chapter_ids=(b,), published_at=时间 + timedelta(days=1), plan_id=plan.plan_id, plan_revision=1, ), ) face = svc.连载工作面(author, req.work_id) assert [c["state"] for c in face["plans"][0]["chapters"]] == ["published", "published"] newer = replace( plan, expected_revision=1, chapters=( replace(plan.chapters[0], scheduled_at=时间 - timedelta(days=1)), plan.chapters[1], ), ) svc.保存连载(author, "reschedule", newer) assert ( svc.读取连载(author, plan.plan_id, 1)["payload"]["chapters"][0]["scheduled_at"] == 时间.isoformat() ) assert svc.读取连载(author, plan.plan_id)["revision"] == 2 @pytest.mark.case_id( "NC-w27-27b002", environment="隔离PG", given="本作者完整目录、确切正文版本与独立隔离数据", when="通过定稿、S02导出、跨章候选或连载反馈实际入口执行并检查失败恢复", then=["反馈未关联不猜近期版本且调整只形成请求"], contract="docs/系统架构/新版设计/模块设计/B08-交付连载.md", ) def test_反馈未关联不猜近期版本且调整只形成请求__27b002(交付环境): app, author, pools, req = 交付环境 svc = app.要求交付() svc.冻结定稿(author, "freeze", req) release = _发布(req) svc.登记发布(author, "release", release) feedback = 读者反馈请求( str(uuid4()), req.work_id, "合成读者来信", "末段看不清是谁进门。", 时间, ) svc.登记读者反馈(author, "unlinked", feedback) linked = replace( feedback, feedback_id=str(uuid4()), release_id=release.release_id, chapter_id=req.chapters[0].chapter_id, category="question", adjustment="请在改稿候选中明确进门的人。", ) svc.登记读者反馈(author, "linked", linked) face = svc.连载工作面(author, req.work_id) rows = {str(r["feedback_id"]): r["payload"] for r in face["feedback"]} assert rows[feedback.feedback_id]["association"] == "unlinked" assert rows[feedback.feedback_id]["chapter_versions"] == [] assert rows[linked.feedback_id]["chapter_versions"][0]["revision"] == 1 assert rows[linked.feedback_id]["causality"] == "not_established" assert rows[linked.feedback_id]["adjustment_state"] == "requested" with pytest.raises(Muse错误, match="章节"): svc.登记读者反馈( author, "wrong-chapter", replace( linked, feedback_id=str(uuid4()), chapter_id=req.chapters[1].chapter_id, ), ) with pools[author.用途].连接(只读=True) as conn: assert conn.execute("SELECT count(*) FROM muse_document_version").fetchone()[0] == 2 assert conn.execute("SELECT count(*) FROM muse_candidate_decision").fetchone()[0] == 0 @pytest.mark.case_id( "NC-w27-27b003", environment="隔离PG", given="本作者完整目录、确切正文版本与独立隔离数据", when="通过定稿、S02导出、跨章候选或连载反馈实际入口执行并检查失败恢复", then=["修订发布产生新记录而旧反馈仍指向原稿"], contract="docs/系统架构/新版设计/模块设计/B08-交付连载.md", ) def test_修订发布产生新记录而旧反馈仍指向原稿__27b003(交付环境): app, author, _, req = 交付环境 svc = app.要求交付() svc.冻结定稿(author, "freeze", req) release = _发布(req) svc.登记发布(author, "release", release) app.要求正文().保存人工( author, "edit", req.chapters[0].chapter_id, 1, 定稿测试.正文测试.草稿("新版第一句"), ) face = svc.全书回看(author, req.work_id) newreq = replace( req, delivery_id=str(uuid4()), chapters=( replace( req.chapters[0], revision=2, document_hash=face["chapters"][0]["document_hash"] ), req.chapters[1], ), ) svc.冻结定稿(author, "new-freeze", newreq) correction = _发布( newreq, corrects_release_id=release.release_id, published_at=时间 + timedelta(days=1), ) svc.登记发布(author, "correction", correction) feedback = 读者反馈请求( str(uuid4()), req.work_id, "旧版读者", "我看到的还是原版。", 时间 + timedelta(days=2), release_id=release.release_id, chapter_id=req.chapters[0].chapter_id, ) svc.登记读者反馈(author, "feedback", feedback) face = svc.连载工作面(author, req.work_id) assert len(face["releases"]) == 2 assert face["feedback"][0]["payload"]["chapter_versions"][0]["revision"] == 1 with pytest.raises(Muse错误): svc.登记发布(author, "release", correction) @pytest.mark.case_id( "NC-w27-27b004", environment="隔离PG", given="本作者完整目录、确切正文版本与独立隔离数据", when="通过定稿、S02导出、跨章候选或连载反馈实际入口执行并检查失败恢复", then=["HTTP从全书回看定稿到发布反馈真实读回"], contract="docs/系统架构/新版设计/模块设计/B08-交付连载.md", ) def test_HTTP从全书回看定稿到发布反馈真实读回__27b004(交付环境, tmp_path): app, author, pools, req = 交付环境 config = 接入测试._配置文件(pools[author.用途], tmp_path) config.write_text(config.read_text().replace("eval-author", author.作者)) with TestClient(创建应用(读取配置(config)), headers={"origin": "http://testserver"}) as client: assert client.get(f"/api/v1/works/{req.work_id}/delivery-draft").status_code == 401 assert ( client.post( "/api/v1/session", json={"password": "synthetic-evaluation-only"} ).status_code == 200 ) face = client.get(f"/api/v1/works/{req.work_id}/delivery-draft") assert face.json()["can_freeze"] is True assert face.json()["literary_status"] == "not_assessed" response = client.post( "/api/v1/deliveries", json={ "command_id": "http-freeze", "delivery": asdict(req), }, ) assert response.status_code == 200, response.text release = asdict(_发布(req)) release["published_at"] = 时间.isoformat() response = client.post( "/api/v1/releases", json={ "command_id": "http-release", "release": release, }, ) assert response.status_code == 200, response.text feedback = asdict( 读者反馈请求( str(uuid4()), req.work_id, "合成留言", "保留原句。", 时间, release_id=release["release_id"], chapter_id=req.chapters[0].chapter_id, ) ) feedback["received_at"] = 时间.isoformat() response = client.post( "/api/v1/reader-feedback", json={ "command_id": "http-reader", "feedback": feedback, }, ) assert response.status_code == 200, response.text rows = client.get(f"/api/v1/works/{req.work_id}/serial").json() assert rows["feedback"][0]["payload"]["release_id"] == release["release_id"] assert app.要求交付().连载工作面(author, req.work_id)["feedback"]