实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
328 lines
14 KiB
Python
328 lines
14 KiB
Python
"""新类型无需前端代码即可读取真实投影、创建未决事实并单独确认。"""
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import time
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from threading import Thread
|
|
|
|
import httpx
|
|
import pytest
|
|
import test_世界维护与影响 as 世界测试
|
|
import uvicorn
|
|
from fastapi.testclient import TestClient
|
|
|
|
from muse.作品规划.接口 import 档案保存
|
|
from muse.元数据.接口 import 值规则, 元数据服务, 字段定义, 字段用途, 类型定义, 结构定义
|
|
from muse.共享.调用身份 import 用途
|
|
from muse.接入.http.应用 import 创建应用
|
|
from muse.资源加载 import 加载清单
|
|
from muse.配置 import 服务配置
|
|
|
|
pytestmark = pytest.mark.数据库
|
|
世界环境 = 世界测试.世界环境
|
|
|
|
|
|
@pytest.fixture
|
|
def 世界表单环境(世界环境, tmp_path):
|
|
app, author, pools = 世界环境
|
|
with pools[用途.维护].连接() as conn, conn.transaction():
|
|
metadata = 元数据服务(conn)
|
|
metadata.登记类型(类型定义("port_cost", "渡口规矩", "world", "entity", ("entity",)))
|
|
schema = 结构定义(
|
|
"port_cost",
|
|
"port_cost_card",
|
|
1,
|
|
(
|
|
字段定义("port.name", "名称", "规矩名称", 值规则("text"), 必填=True),
|
|
字段定义("port.cost", "代价", "渡河代价", 值规则("text")),
|
|
字段定义(
|
|
"port.note",
|
|
"说明",
|
|
"只读说明",
|
|
值规则("text"),
|
|
用途=字段用途(userEditable=False),
|
|
),
|
|
),
|
|
)
|
|
metadata.登记结构候选(schema)
|
|
metadata.发布结构(schema.schema_id, 1, schema.内容哈希)
|
|
metadata.登记结构候选(replace(schema, schema_version=2))
|
|
secret = tmp_path / "author-password"
|
|
secret.write_text("synthetic-world-form")
|
|
secret.chmod(0o600)
|
|
config = replace(
|
|
app.配置,
|
|
资源发布身份=加载清单()["构建身份"],
|
|
HTTP=服务配置(
|
|
str(secret),
|
|
作者ID=author.作者,
|
|
公开地址="http://testserver",
|
|
允许来源=("http://testserver",),
|
|
),
|
|
)
|
|
with TestClient(创建应用(config), headers={"Origin": "http://testserver"}) as client:
|
|
assert (
|
|
client.post("/api/v1/session", json={"password": secret.read_text()}).status_code == 200
|
|
)
|
|
yield app, author, pools, client
|
|
|
|
|
|
def 新表单(client, object_id="port-entity"):
|
|
response = client.get(
|
|
f"/api/v1/works/story-work/world/entities/{object_id}/form",
|
|
params={"schema_id": "port_cost_card", "base_version": 1},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
return response.json()
|
|
|
|
|
|
def 提案请求(form, content, command):
|
|
return {
|
|
"command_id": command,
|
|
"proposal": {
|
|
"work_id": form["work_id"],
|
|
"object_id": form["object_id"],
|
|
"expected_revision": form["revision"],
|
|
"schema": form["schema_binding"],
|
|
"content": content,
|
|
**form["context"],
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.mark.case_id(
|
|
"NC-w29-29e101",
|
|
environment="隔离PG",
|
|
given="显式作者、隔离配置、合成数据与已知发布构建",
|
|
when="经真实维护、业务或浏览器入口执行并读取持久结果",
|
|
then=["动态类型实际投影到提案审阅确认及编辑"],
|
|
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md#作者实体表单与字段提案",
|
|
)
|
|
def test_动态类型实际投影到提案审阅确认及编辑__29e101(世界表单环境):
|
|
app, author, _, client = 世界表单环境
|
|
types = client.get("/api/v1/world/types").json()
|
|
registered = next(t for t in types if t["type_id"] == "port_cost")
|
|
assert registered["schemas"] == [{"schema_id": "port_cost_card", "base_version": 1}]
|
|
form = 新表单(client)
|
|
assert form["revision"] == 0 and form["type_id"] == "port_cost"
|
|
assert "说明" in form["visible_schema"]["properties"]
|
|
assert "说明" not in form["editable_schema"]["properties"]
|
|
req = 提案请求(form, {"名称": "过河", "代价": "留下一段记忆"}, "propose-port")
|
|
saved = client.post("/api/v1/works/story-work/fact-proposals", json=req)
|
|
assert saved.status_code == 200, saved.text
|
|
assert client.post("/api/v1/works/story-work/fact-proposals", json=req).json() == saved.json()
|
|
assert app.要求故事世界().列出世界实体(author, "story-work") == []
|
|
pid = saved.json()["results"][0]["proposal_id"]
|
|
proposal = client.get("/api/v1/fact-proposals/" + pid).json()
|
|
assert proposal["decision"] == "undecided" and proposal["content"] == req["proposal"]["content"]
|
|
location = {
|
|
"proposal_id": pid,
|
|
"proposal_revision": proposal["revision"],
|
|
"proposal_hash": proposal["proposal_hash"],
|
|
"target_ref": "world:port-entity",
|
|
"expected_revision": 0,
|
|
}
|
|
review = client.post(f"/api/v1/fact-proposals/{pid}/reviews", json=location)
|
|
assert review.status_code == 200, review.text
|
|
assert review.json()["proposal"]["content"] == proposal["content"]
|
|
decision = {
|
|
**location,
|
|
"command_id": "adopt-port",
|
|
"action": "adopt",
|
|
"author_review_id": review.json()["review_id"],
|
|
"review_hash": review.json()["review_hash"],
|
|
"approved_changes": ["object"],
|
|
}
|
|
receipt = client.post(f"/api/v1/fact-proposals/{pid}/decisions", json=decision)
|
|
assert receipt.status_code == 200, receipt.text
|
|
assert (
|
|
client.post(f"/api/v1/fact-proposals/{pid}/decisions", json=decision).json()
|
|
== receipt.json()
|
|
)
|
|
current = client.get("/api/v1/works/story-work/world/entities/port-entity/form").json()
|
|
assert current["revision"] == 1 and current["content"] == proposal["content"]
|
|
edit = 提案请求(current, {}, "edit-port")
|
|
edit["proposal"]["changes"] = [
|
|
{"路径": ["代价"], "动作": "remove"},
|
|
{"路径": ["名称"], "动作": "set", "值": "免费渡河"},
|
|
]
|
|
changed = client.post("/api/v1/works/story-work/fact-proposals", json=edit)
|
|
assert changed.status_code == 200, changed.text
|
|
assert app.要求故事世界().列出世界实体(author, "story-work")[0]["revision"] == 1
|
|
changed_pid = changed.json()["results"][0]["proposal_id"]
|
|
changed_proposal = client.get("/api/v1/fact-proposals/" + changed_pid).json()
|
|
assert changed_proposal["content"] == {"名称": "免费渡河"}
|
|
世界测试.确认提案(app, author, changed.json(), "port-entity", expected=1, command="edit-adopt")
|
|
assert (
|
|
client.post("/api/v1/works/story-work/fact-proposals", json=edit).json() == changed.json()
|
|
)
|
|
final = client.get("/api/v1/works/story-work/world/entities/port-entity/form").json()
|
|
assert final["content"] == {"名称": "免费渡河"} and final["revision"] == 2
|
|
assert final["schema_binding"]["base_version"] == 1
|
|
|
|
|
|
@pytest.mark.case_id(
|
|
"NC-w29-29e102",
|
|
environment="隔离PG",
|
|
given="显式作者、隔离配置、合成数据与已知发布构建",
|
|
when="经真实维护、业务或浏览器入口执行并读取持久结果",
|
|
then=["跨作品实体表单及旧投影写入拒绝"],
|
|
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md#作者实体表单与字段提案",
|
|
)
|
|
def test_跨作品实体表单及旧投影写入拒绝__29e102(世界表单环境):
|
|
app, author, pools, client = 世界表单环境
|
|
form = 新表单(client)
|
|
bad = 提案请求(form, {"名称": "过河", "说明": "绕过只读字段"}, "readonly-write")
|
|
denied = client.post("/api/v1/works/story-work/fact-proposals", json=bad)
|
|
assert denied.status_code == 422 and denied.json()["code"] == "FIELD_NOT_ALLOWED"
|
|
stale = 提案请求(form, {"名称": "过河"}, "stale-projection")
|
|
stale["proposal"]["schema"]["projection_version"] = "stale"
|
|
denied = client.post("/api/v1/works/story-work/fact-proposals", json=stale)
|
|
assert denied.json()["code"] == "PROJECTION_STALE"
|
|
with pools[用途.维护].连接() as conn, conn.transaction():
|
|
meta = 元数据服务(conn)
|
|
unpublished = replace(meta.读取结构("port_cost_card", 1), schema_version=2)
|
|
meta.发布结构("port_cost_card", 2, unpublished.内容哈希)
|
|
# 发布新版不修改现有表单的固定基础版本,旧投影不能凭造的哈希冒充新版。
|
|
wrong = client.get(
|
|
"/api/v1/works/story-work/world/entities/port-entity/form",
|
|
params={"schema_id": "port_cost_card", "base_version": 9},
|
|
)
|
|
assert wrong.status_code != 200
|
|
assert wrong.json()["code"] == "SCHEMA_UNKNOWN"
|
|
app.要求作品().保存档案(
|
|
author,
|
|
"other-world",
|
|
档案保存(
|
|
"other-world",
|
|
0,
|
|
{"名称": "另一作品"},
|
|
app.要求作品().新档案结构(author, "other-world", "work_core", 1),
|
|
),
|
|
)
|
|
saved = app.要求故事世界().保存实体提案(
|
|
author,
|
|
"actual-entity",
|
|
work_id="story-work",
|
|
object_id="existing-port",
|
|
expected_revision=0,
|
|
schema_id="port_cost_card",
|
|
schema_version=1,
|
|
content={"名称": "本书规矩"},
|
|
)
|
|
世界测试.确认提案(app, author, saved, "existing-port")
|
|
cross = client.get("/api/v1/works/other-world/world/entities/existing-port/form")
|
|
assert cross.status_code != 200
|
|
assert cross.json()["code"] == "SCOPE_DENIED"
|
|
|
|
|
|
@pytest.mark.case_id(
|
|
"NC-w29-29e103",
|
|
environment="隔离PG",
|
|
given="显式作者、隔离配置、合成数据与已知发布构建",
|
|
when="经真实维护、业务或浏览器入口执行并读取持久结果",
|
|
then=["实体保存HTTP接收完整具名载荷且只建立候选"],
|
|
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md#作者实体表单与字段提案",
|
|
)
|
|
def test_实体保存HTTP接收完整具名载荷且只建立候选__29e103(世界表单环境):
|
|
app, author, _, client = 世界表单环境
|
|
payload = {
|
|
"command_id": "world-entity-http",
|
|
"object_id": "http-port",
|
|
"expected_revision": 0,
|
|
"schema_id": "port_cost_card",
|
|
"schema_version": 1,
|
|
"content": {"名称": "渡口规矩", "代价": "一枚铜钱"},
|
|
"independent": True,
|
|
"knowledge_mode": "setting",
|
|
}
|
|
response = client.post("/api/v1/works/story-work/world/entities", json=payload)
|
|
assert response.status_code == 201, response.text
|
|
assert (
|
|
client.post("/api/v1/works/story-work/world/entities", json=payload).json()
|
|
== response.json()
|
|
)
|
|
pid = response.json()["results"][0]["proposal_id"]
|
|
proposal = app.要求故事世界().读取提案(author, pid)
|
|
assert proposal["decision"] == "undecided" and proposal["content"] == payload["content"]
|
|
assert app.要求故事世界().列出世界实体(author, "story-work") == []
|
|
rejected = client.post(
|
|
"/api/v1/works/story-work/world/entities",
|
|
json={**payload, "decided_by": "admin"},
|
|
)
|
|
assert rejected.status_code == 422
|
|
|
|
|
|
@pytest.mark.case_id(
|
|
"NC-w29-29e104",
|
|
environment="隔离PG",
|
|
given="显式作者、隔离配置、合成数据与已知发布构建",
|
|
when="经真实维护、业务或浏览器入口执行并读取持久结果",
|
|
then=["新类型经真实浏览器保存确认刷新再编辑"],
|
|
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md#作者实体表单与字段提案",
|
|
)
|
|
@pytest.mark.浏览器
|
|
def test_新类型经真实浏览器保存确认刷新再编辑__29e104(世界表单环境, tmp_path):
|
|
if os.environ.get("MUSE_RUN_BROWSER") != "1":
|
|
pytest.skip("需显式启用浏览器")
|
|
app, author, _, client = 世界表单环境
|
|
original = app.要求正文().读取正文(author, "chapter-1")
|
|
with socket.socket() as sock:
|
|
sock.bind(("127.0.0.1", 0))
|
|
port = sock.getsockname()[1]
|
|
base = f"http://127.0.0.1:{port}"
|
|
config = client.app.state.装配.配置
|
|
config = replace(config, HTTP=replace(config.HTTP, 公开地址=base, 允许来源=(base,)))
|
|
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, exist_ok=True)
|
|
try:
|
|
for _ in range(100):
|
|
if server.started:
|
|
break
|
|
time.sleep(0.05)
|
|
assert server.started and httpx.get(base + "/ready", trust_env=False).status_code == 200
|
|
environment = {
|
|
**os.environ,
|
|
"MUSE_WORKBENCH_URL": base,
|
|
"MUSE_WORK_ID": "story-work",
|
|
"MUSE_AUTHOR_PASSWORD_FILE": config.HTTP.口令文件,
|
|
"MUSE_BROWSER_EXECUTABLE": (
|
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
),
|
|
}
|
|
with (output / "浏览器.json").open("w") as report:
|
|
run = 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=90,
|
|
)
|
|
assert run.returncode == 0, "浏览器未通过,见浏览器.json"
|
|
current = app.要求故事世界().列出世界实体(author, "story-work")
|
|
assert len(current) == 1 and current[0]["revision"] == 2
|
|
assert current[0]["content"] == {"名称": "过河的规矩", "代价": "一枚旧铜钱"}
|
|
assert app.要求正文().读取正文(author, "chapter-1") == original
|
|
finally:
|
|
server.should_exit = True
|
|
thread.join(10)
|
|
assert not thread.is_alive()
|