muse-agent-example/tests/集成/test_上下文工作台.py
zizi d909d1bd1b 后端实现与用例身份:19 包集成落地并修复收尾缺陷
实现侧:
- 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。
- 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。
- 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。
- 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。
- 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。
- 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。
- 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。
- 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。

用例侧:
- 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存;
- 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
2026-09-18 01:15:00 +08:00

222 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""作者工作台按任务读取上下文快照的 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.接入.http.应用 import 创建应用
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
from muse.配置 import 应用配置, 服务配置
pytestmark = pytest.mark.数据库
@pytest.fixture
def 工作台环境(内置结构测试库, tmp_path):
"""真实作品、任务与冻结快照;HTTP 层只读消费,不另造第二套数据。"""
应用测试库 = 内置结构测试库
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "test"))
with 装配.生命周期():
作者 = 调用身份("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",)
),
)
yield {
"装配": 装配,
"task_id": task_id,
"他人任务": 他人任务,
"配置": 配置,
"口令": 口令文件.read_text(),
}
@pytest.mark.case_id(
"NC-ctx-workbench-author-view",
environment="真实隔离PostgreSQL",
given="作者经会话查询已有快照任务",
when="GET /api/v1/tasks/{id}/context-snapshots",
then=["未登录 401;作者可见快照的材料清单(入选/字节数/省略原因)与逐来源版本、哈希、投影版本"],
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md",
)
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
@pytest.mark.case_id(
"NC-ctx-workbench-denied",
environment="真实隔离PostgreSQL",
given="他人作者会话或不存在的任务",
when="GET /api/v1/tasks/{id}/context-snapshots",
then=["他人任务 403 SCOPE_DENIED;不存在任务 422;不泄露内容"],
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md",
)
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"
@pytest.mark.case_id(
"NC-ctx-workbench-drift-visible",
environment="真实隔离PostgreSQL",
given="快照冻结后来源漂移",
when="再次 GET 上下文快照",
then=["同一 snapshot_id 与 input_hash;失效说明与具体变化项可见;历史装配与来源绑定原样"],
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md",
)
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 记录