193 lines
8.7 KiB
Python
193 lines
8.7 KiB
Python
"""作者工作台按任务读取上下文快照的 HTTP 合同:材料清单、来源证据与失效说明。
|
||
|
||
真实隔离 PG + 真实 ASGI;只读消费回合1–3 的上下文服务,不 mock 后端。
|
||
清单只暴露取舍与绑定身份,不回传装配全文,也不泄漏连接串或内部异常。
|
||
"""
|
||
|
||
from uuid import uuid4
|
||
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
|
||
from muse.上下文.接口 import 投影版本, 来源绑定, 装配材料
|
||
from muse.任务运行.接口 import 任务服务, 任务请求, 步骤处理器, 步骤结果, 步骤计划
|
||
from muse.作品规划.接口 import 档案保存
|
||
from muse.元数据.接口 import 元数据服务, 导入内置结构
|
||
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
|
||
from muse.启动 import 构建
|
||
from muse.接入.http.应用 import 创建应用
|
||
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
|
||
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
|
||
from muse.配置 import 应用配置, 服务配置
|
||
|
||
pytestmark = pytest.mark.数据库
|
||
|
||
|
||
@pytest.fixture
|
||
def 工作台环境(应用测试库, tmp_path):
|
||
"""真实作品、任务与冻结快照;HTTP 层只读消费,不另造第二套数据。"""
|
||
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
|
||
导入内置结构(元数据服务(连))
|
||
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "test"))
|
||
作者 = 调用身份("author-local", None, 用途.生产, 内容用途.生成)
|
||
作品 = 装配.要求作品()
|
||
作品.保存档案(
|
||
作者,
|
||
"create-wb-work",
|
||
档案保存(
|
||
"wb-work",
|
||
0,
|
||
{"名称": "材料台账", "题材定位": "现实", "主题立意": "对账"},
|
||
作品.新档案结构(作者, "wb-work", "work_core", 1),
|
||
),
|
||
)
|
||
作品.添加章节(作者, "add-ch-1", "wb-work", "ch-1", "第一章", 预期目录版本=0)
|
||
装配.要求正文().保存人工(
|
||
作者, "body-ch-1", "ch-1", 0, 正文草稿((段落("p-1", (文本节点("林深把伞收了起来。"),)),))
|
||
)
|
||
范围 = 装配.要求上下文().授权任务范围(
|
||
作者, "wb-work", 1, ({"chapter_id": "ch-1", "revision": 1},)
|
||
)
|
||
登记 = 流程登记()
|
||
登记.登记处理器(
|
||
步骤处理器("workbench-freeze", "1", lambda _: 步骤结果({"ok": True}), "v1", "v1")
|
||
)
|
||
登记.登记类型("workbench-freeze", 必需保护=())
|
||
运行 = 任务服务(应用测试库[用途.生产], 登记)
|
||
流程服务(运行, 登记).发布(
|
||
流程定义("workbench-freeze", "1", (步骤计划("冻结", "workbench-freeze", "1"),))
|
||
)
|
||
|
||
def 建任务(作者ID: str, 冻结范围=None) -> str:
|
||
冻结 = {
|
||
"source_scope": 冻结范围 if 冻结范围 is not None else {"work_id": "wb-work"},
|
||
"schema_versions": {},
|
||
"authorization": "workbench-synthetic",
|
||
"budget": {"context_bytes": 4096},
|
||
"stop_conditions": {},
|
||
}
|
||
return 运行.创建任务(
|
||
任务请求(
|
||
"workbench-freeze",
|
||
f"create-{作者ID}-{uuid4().hex[:6]}",
|
||
作者ID,
|
||
用途.生产,
|
||
内容用途.生成,
|
||
{},
|
||
"test",
|
||
"test",
|
||
冻结,
|
||
作品ID="wb-work",
|
||
),
|
||
"workbench-freeze",
|
||
"1",
|
||
)
|
||
|
||
task_id = 建任务("author-local", 范围)
|
||
他人任务 = 建任务("someone-else")
|
||
材料 = {"历史:ch-1": "林深把伞收了起来。", "参考:超长素材": "很" * 4000}
|
||
文稿 = 范围["documents"][0]
|
||
绑定 = (
|
||
来源绑定(
|
||
文稿["document_id"],
|
||
str(文稿["revision"]),
|
||
文稿["document_hash"],
|
||
投影版本("production", "generation"),
|
||
),
|
||
)
|
||
装配.要求上下文().冻结上下文(task_id, 装配材料(材料, ("历史:ch-1",), 字节上限=4096), 绑定)
|
||
口令文件 = tmp_path / "作者口令.txt"
|
||
口令文件.write_text("workbench-password")
|
||
配置 = 应用配置(
|
||
应用测试库[用途.生产].引用,
|
||
"test",
|
||
HTTP=服务配置(str(口令文件), 公开地址="http://testserver", 允许来源=("http://testserver",)),
|
||
)
|
||
return {
|
||
"装配": 装配,
|
||
"task_id": task_id,
|
||
"他人任务": 他人任务,
|
||
"配置": 配置,
|
||
"口令": 口令文件.read_text(),
|
||
}
|
||
|
||
|
||
def test_作者可见任务材料清单与来源证据__dd023f(工作台环境):
|
||
环境 = 工作台环境
|
||
task_id = 环境["task_id"]
|
||
with TestClient(创建应用(环境["配置"])) as client:
|
||
未登录 = client.get(f"/api/v1/tasks/{task_id}/context-snapshots")
|
||
assert 未登录.status_code == 401
|
||
client.headers["Origin"] = "http://testserver"
|
||
assert client.post("/api/v1/session", json={"password": 环境["口令"]}).status_code == 200
|
||
快照集 = client.get(f"/api/v1/tasks/{task_id}/context-snapshots")
|
||
assert 快照集.status_code == 200
|
||
assert len(快照集.json()) == 1
|
||
快照 = 快照集.json()[0]
|
||
assert 快照["task_id"] == task_id and 快照["work_id"] == "wb-work"
|
||
assert 快照["snapshot_id"] and len(快照["input_hash"]) == 64
|
||
assert 快照["state"] == "有效"
|
||
assert 快照["invalidation"] is None and 快照["changes"] == []
|
||
清单 = 快照["assembly"]
|
||
assert 清单["included_sources"] == ["历史:ch-1"]
|
||
assert 清单["omissions"] == [{"source": "参考:超长素材", "reason": "BUDGET_EXCEEDED"}]
|
||
assert 清单["byte_size"] == len("林深把伞收了下来。".encode())
|
||
assert len(清单["text_hash"]) == 64
|
||
来源 = 快照["sources"]
|
||
assert len(来源) == 1
|
||
文稿 = 范围文稿(环境)
|
||
assert 来源[0] == {
|
||
"source_id": 文稿["document_id"],
|
||
"data_version": "1",
|
||
"schema_hash": 文稿["document_hash"],
|
||
"projection_version": "visible-text-codepoints/v1:production:generation",
|
||
"current_state": "有效",
|
||
}
|
||
# 清单只给取舍与身份,不回传装配全文
|
||
assert "text" not in 清单
|
||
assert "林深把伞收了下来" not in 快照集.text
|
||
|
||
|
||
def test_他人任务与不存在任务被拒绝__5f6302(工作台环境):
|
||
环境 = 工作台环境
|
||
with TestClient(创建应用(环境["配置"])) as client:
|
||
client.headers["Origin"] = "http://testserver"
|
||
assert client.post("/api/v1/session", json={"password": 环境["口令"]}).status_code == 200
|
||
他人 = client.get(f"/api/v1/tasks/{环境['他人任务']}/context-snapshots")
|
||
assert 他人.status_code == 403
|
||
assert 他人.json()["code"] == "SCOPE_DENIED"
|
||
不存在 = client.get("/api/v1/tasks/00000000-0000-0000-0000-000000000000/context-snapshots")
|
||
assert 不存在.status_code == 422
|
||
assert 不存在.json()["code"] == "MUSE_TASK"
|
||
|
||
|
||
def test_来源漂移后失效说明可见且历史装配原样__18a1af(工作台环境):
|
||
环境 = 工作台环境
|
||
task_id = 环境["task_id"]
|
||
with TestClient(创建应用(环境["配置"])) as client:
|
||
client.headers["Origin"] = "http://testserver"
|
||
assert client.post("/api/v1/session", json={"password": 环境["口令"]}).status_code == 200
|
||
冻结前 = client.get(f"/api/v1/tasks/{task_id}/context-snapshots").json()[0]
|
||
作者 = 调用身份("author-local", None, 用途.生产, 内容用途.生成)
|
||
环境["装配"].要求正文().保存人工(
|
||
作者, "wb-rev-2", "ch-1", 1, 正文草稿((段落("p-2", (文本节点("第二天清晨。"),)),))
|
||
)
|
||
漂移后集 = client.get(f"/api/v1/tasks/{task_id}/context-snapshots").json()
|
||
assert len(漂移后集) == 1
|
||
漂移后 = 漂移后集[0]
|
||
assert 漂移后["snapshot_id"] == 冻结前["snapshot_id"]
|
||
assert 漂移后["input_hash"] == 冻结前["input_hash"]
|
||
assert 漂移后["state"] == "SOURCE_STALE"
|
||
assert 漂移后["invalidation"] and "ch-1" in 漂移后["invalidation"]
|
||
assert any("前进" in 变化 for 变化 in 漂移后["changes"])
|
||
assert 漂移后["assembly"] == 冻结前["assembly"]
|
||
assert 漂移后["sources"][0]["source_id"] == 冻结前["sources"][0]["source_id"]
|
||
assert 漂移后["sources"][0]["data_version"] == "1"
|
||
assert 漂移后["sources"][0]["current_state"] == "SOURCE_STALE"
|
||
|
||
|
||
def 范围文稿(环境) -> dict:
|
||
作者 = 调用身份("author-local", None, 用途.生产, 内容用途.生成)
|
||
记录 = 环境["装配"].要求正文().读取正文(作者, "ch-1")
|
||
return 记录
|