实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
494 lines
20 KiB
Python
494 lines
20 KiB
Python
"""真实 ASGI、CLI 与隔离 PostgreSQL 共用任务合同;不使用仓储替身。"""
|
||
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
|
||
from muse.任务运行.接口 import 任务服务, 任务请求, 步骤处理器, 步骤结果, 步骤计划
|
||
from muse.共享.调用身份 import 内容用途, 用途
|
||
from muse.接入.http.应用 import 创建应用
|
||
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
|
||
from muse.配置 import 应用配置, 服务配置
|
||
|
||
pytestmark = pytest.mark.数据库
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-793c2a5dc91b",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,JSON 属性顺序与命令模型顺序不同",
|
||
when="用中文命令身份提交取消请求",
|
||
then=["字段按名称绑定,中文命令身份原样返回", "只取消指定任务,真实库内状态为 cancelled"],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
)
|
||
def test_具名CLI按字段传参不依赖JSON排列__793c2a(任务接入环境, tmp_path: Path) -> None:
|
||
服务, 身份, _, 配置文件 = 任务接入环境
|
||
请求文件 = tmp_path / "作者决定.json"
|
||
请求文件.write_text(
|
||
json.dumps(
|
||
{
|
||
"action": "取消",
|
||
"expected_state": "queued",
|
||
"target_ref": 身份,
|
||
"command_id": "作者决定-1",
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
)
|
||
进程 = subprocess.run(
|
||
[sys.executable, "-m", "muse", "任务", str(配置文件), "控制", str(请求文件)],
|
||
cwd="/",
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=20,
|
||
)
|
||
assert 进程.returncode == 0, 进程.stderr
|
||
assert json.loads(进程.stdout)["command_id"] == "作者决定-1"
|
||
assert 服务.读取任务(身份).状态.value == "cancelled"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"请求文本,额外参数,退出码",
|
||
[
|
||
pytest.param(
|
||
"{}",
|
||
[],
|
||
1,
|
||
id="empty",
|
||
marks=pytest.mark.case_id(
|
||
"TC-a153a064ff27",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 empty",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
'["x", 1, null]',
|
||
[],
|
||
1,
|
||
id="typed-array",
|
||
marks=pytest.mark.case_id(
|
||
"TC-3da3da1671b2",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 typed-array",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
"{}",
|
||
["--stdin"],
|
||
2,
|
||
id="competing-input",
|
||
marks=pytest.mark.case_id(
|
||
"TC-d32b80af9e77",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 competing-input",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
"not json",
|
||
[],
|
||
1,
|
||
id="invalid-json",
|
||
marks=pytest.mark.case_id(
|
||
"TC-7ec3e69f2c3a",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 invalid-json",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
'{"a": 1}',
|
||
[],
|
||
1,
|
||
id="unknown-object",
|
||
marks=pytest.mark.case_id(
|
||
"TC-b9ed13bfb07c",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 unknown-object",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
'{"sql": "CREATE TABLE t (id int)"}',
|
||
[],
|
||
1,
|
||
id="create-sql",
|
||
marks=pytest.mark.case_id(
|
||
"TC-fdcd652d4027",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 create-sql",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
'{"sql": "DELETE FROM t"}',
|
||
[],
|
||
1,
|
||
id="bare-delete",
|
||
marks=pytest.mark.case_id(
|
||
"TC-8444ef0d07e5",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 bare-delete",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
'{"sql": "ALTER TABLE t ADD COLUMN x int", "params": []}',
|
||
[],
|
||
1,
|
||
id="alter-params",
|
||
marks=pytest.mark.case_id(
|
||
"TC-ae483faef69c",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 alter-params",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
'{"sql": "DELETE FROM t", "params": []}',
|
||
[],
|
||
1,
|
||
id="delete-params",
|
||
marks=pytest.mark.case_id(
|
||
"TC-d34bae229e80",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 delete-params",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
'{"sql": "UPDATE t SET x=1 WHERE id=1"}',
|
||
[],
|
||
1,
|
||
id="update-where",
|
||
marks=pytest.mark.case_id(
|
||
"TC-ed53c61a1e2d",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 update-where",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
pytest.param(
|
||
'{"sql": "DELETE FROM t WHERE id=%s", "params": [1]}',
|
||
[],
|
||
1,
|
||
id="delete-where",
|
||
marks=pytest.mark.case_id(
|
||
"TC-478d7e025686",
|
||
environment="隔离 PostgreSQL、真实 CLI",
|
||
given="已有排队任务,独立输入分支 delete-where",
|
||
when="从任意 cwd 向具名任务控制 CLI 提交该输入",
|
||
then=[
|
||
(
|
||
"未登记的参数来源、JSON 外壳或 SQL 能力被拒"
|
||
"绝,退出码为用法错误或 INVALID_REQUEST"
|
||
),
|
||
"任务仍为 queued,没有业务写副作用",
|
||
],
|
||
contract="docs/系统架构/新版设计/接口契约/命令与作者决策.md",
|
||
),
|
||
),
|
||
],
|
||
ids=[
|
||
"empty",
|
||
"typed-array",
|
||
"competing-input",
|
||
"invalid-json",
|
||
"unknown-object",
|
||
"create-sql",
|
||
"bare-delete",
|
||
"alter-params",
|
||
"delete-params",
|
||
"update-where",
|
||
"delete-where",
|
||
],
|
||
)
|
||
def test_具名CLI拒绝未登记输入且任务不变__a71002(
|
||
任务接入环境,
|
||
tmp_path: Path,
|
||
请求文本: str,
|
||
额外参数: list[str],
|
||
退出码: int,
|
||
) -> None:
|
||
"""旧通用 SQL 入口已退出;只接受具名命令合同,不能以 WHERE 或参数列表换取写能力。"""
|
||
服务, 身份, _, 配置文件 = 任务接入环境
|
||
请求文件 = tmp_path / "请求.json"
|
||
请求文件.write_text(请求文本)
|
||
进程 = subprocess.run(
|
||
[sys.executable, "-m", "muse", "任务", str(配置文件), "控制", str(请求文件), *额外参数],
|
||
cwd="/",
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=20,
|
||
)
|
||
assert 进程.returncode == 退出码, 进程.stdout
|
||
if 退出码 == 1:
|
||
assert json.loads(进程.stderr)["code"] == "INVALID_REQUEST"
|
||
assert 服务.读取任务(身份).状态.value == "queued"
|
||
|
||
|
||
@pytest.fixture
|
||
def 任务接入环境(应用测试库, tmp_path: Path):
|
||
registry = 流程登记()
|
||
registry.登记处理器(步骤处理器("synthetic", "1", lambda _: 步骤结果({"ok": True}), "v1", "v1"))
|
||
registry.登记类型("synthetic", 必需保护=())
|
||
service = 任务服务(应用测试库[用途.生产], registry)
|
||
flow = 流程服务(service, registry)
|
||
flow.发布(流程定义("synthetic", "1", (步骤计划("合成步骤", "synthetic", "1"),)))
|
||
req = 任务请求(
|
||
"合成任务",
|
||
"create",
|
||
"author-local",
|
||
用途.生产,
|
||
内容用途.抽取,
|
||
{},
|
||
"v1",
|
||
"test",
|
||
{
|
||
"source_scope": {},
|
||
"schema_versions": {},
|
||
"authorization": "synthetic",
|
||
"budget": {},
|
||
"stop_conditions": [],
|
||
},
|
||
作品ID="work-A",
|
||
)
|
||
task_id = service.创建任务(req, "synthetic", "1")
|
||
token = tmp_path / "口令.txt"
|
||
token.write_text("synthetic-password")
|
||
config = 应用配置(
|
||
应用测试库[用途.生产].引用,
|
||
"test",
|
||
HTTP=服务配置(str(token), 公开地址="http://testserver", 允许来源=("http://testserver",)),
|
||
)
|
||
file = tmp_path / "应用.toml"
|
||
file.write_text(f"""["数据库"]
|
||
"取值方式" = "受控存储"
|
||
"位置" = {json.dumps(应用测试库[用途.生产].引用.位置)}
|
||
["资源"]
|
||
"发布身份" = "test"
|
||
[HTTP]
|
||
"口令文件" = {json.dumps(str(token))}
|
||
""")
|
||
return service, task_id, config, file
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-http-cli-task-replay",
|
||
environment="隔离PostgreSQL与真实ASGI/CLI",
|
||
given="同一个作者和持久化任务",
|
||
when="HTTP控制、CLI重放、错误范围与事件续接",
|
||
then=["回执与错误一致,去重且scope拒绝"],
|
||
contract=".agent/rules/测试隔离.md",
|
||
)
|
||
def test_HTTP与CLI控制回执和拒绝保持一致__a71001(任务接入环境, tmp_path: Path) -> None:
|
||
service, task_id, config, file = 任务接入环境
|
||
with TestClient(创建应用(config)) as client:
|
||
assert client.get(f"/api/v1/tasks/{task_id}").status_code == 401
|
||
client.headers["Origin"] = "http://testserver"
|
||
assert (
|
||
client.post("/api/v1/session", json={"password": "synthetic-password"}).status_code
|
||
== 200
|
||
)
|
||
assert (
|
||
client.get("/api/v1/tasks", params={"work_id": "work-A"}).json()[0]["task_id"]
|
||
== task_id
|
||
)
|
||
assert (
|
||
client.get(f"/api/v1/tasks/{task_id}", params={"work_id": "work-B"}).status_code == 403
|
||
)
|
||
body = {
|
||
"command_id": "cancel-once",
|
||
"target_ref": task_id,
|
||
"expected_state": "queued",
|
||
"action": "取消",
|
||
}
|
||
response = client.post(f"/api/v1/tasks/{task_id}/controls", json=body)
|
||
assert response.status_code == 200
|
||
request = tmp_path / "请求.json"
|
||
request.write_text(json.dumps(body))
|
||
args = [sys.executable, "-m", "muse", "任务", str(file), "控制", str(request)]
|
||
cli = subprocess.run(args, cwd="/", capture_output=True, text=True, timeout=20)
|
||
assert cli.returncode == 0, cli.stderr
|
||
assert json.loads(cli.stdout) == response.json()
|
||
body["action"] = "暂停"
|
||
request.write_text(json.dumps(body))
|
||
response = client.post(f"/api/v1/tasks/{task_id}/controls", json=body)
|
||
cli = subprocess.run(args, cwd="/", capture_output=True, text=True, timeout=20)
|
||
assert response.status_code == 409 and cli.returncode == 1
|
||
assert response.json() == json.loads(cli.stderr)
|
||
events = client.get(f"/api/v1/tasks/{task_id}/events").json()
|
||
assert [event["event_type"] for event in events["items"]] == [
|
||
"task.created",
|
||
"task.cancelled",
|
||
]
|
||
assert (
|
||
client.get(f"/api/v1/tasks/{task_id}/events", params={"cursor": 2}).json()["items"]
|
||
== []
|
||
)
|
||
|
||
|
||
@pytest.mark.case_id("NC-O02-HTTP-TERMINAL-SETTLEMENT")
|
||
def test_HTTP取消后结账与CLI读回不复活(任务接入环境, tmp_path):
|
||
import hashlib
|
||
from datetime import UTC, datetime, timedelta
|
||
from decimal import Decimal
|
||
|
||
from muse.任务运行.接口 import 任务状态, 任务预算计划, 角色预算, 预算管理, 额度策略
|
||
|
||
service, task_id, config, file = 任务接入环境
|
||
lease = service.领取步骤(
|
||
"settlement-test", ["synthetic"], 任务ID=task_id, 作者=config.HTTP.作者ID
|
||
)
|
||
assert lease is not None
|
||
budget = 预算管理(service.数据库, "settlement-account")
|
||
budget.登记策略(额度策略("settlement-account", "1", Decimal("2"), 2))
|
||
budget.登记任务预算(
|
||
task_id,
|
||
任务预算计划(
|
||
Decimal("1"),
|
||
(角色预算("writer", 1, 1, Decimal("1")),),
|
||
"author-approved",
|
||
datetime.now(UTC) + timedelta(minutes=5),
|
||
),
|
||
)
|
||
budget.预留(lease, "call-for-settlement", "writer")
|
||
budget.标记已发送(lease, "call-for-settlement", 最长秒=30, 登记尝试=True)
|
||
service.控制任务(task_id, config.HTTP.作者ID, 任务状态.运行中, "取消", 命令ID="cancel-paid")
|
||
before = service.读取任务(task_id)
|
||
statement = "作者确认账单第3项的调用已发生,实付0.2美元。"
|
||
body = {
|
||
"command_id": "settle-cancelled",
|
||
"expected_revision": before.账目版本,
|
||
"amount": "0.2",
|
||
"currency": "USD",
|
||
"source_reference": "账单第3项",
|
||
"source_statement": statement,
|
||
"source_hash": hashlib.sha256(statement.encode()).hexdigest(),
|
||
}
|
||
with TestClient(创建应用(config)) as client:
|
||
client.headers["Origin"] = "http://testserver"
|
||
assert (
|
||
client.post("/api/v1/session", json={"password": "synthetic-password"}).status_code
|
||
== 200
|
||
)
|
||
url = f"/api/v1/tasks/{task_id}/calls/call-for-settlement/settlements"
|
||
response = client.post(url, json=body)
|
||
assert response.status_code == 200, response.text
|
||
assert client.post(url, json=body).json() == response.json()
|
||
assert client.post(url, json={**body, "amount": "0.3"}).status_code == 409
|
||
current = client.get(f"/api/v1/tasks/{task_id}").json()
|
||
assert current["state"] == "cancelled" and current["last_sequence"] == before.最后序号
|
||
assert current["execution_final_sequence"] == before.最后序号
|
||
assert current["accounting_revision"] == before.账目版本 + 1
|
||
calls = client.get(f"/api/v1/tasks/{task_id}/calls").json()
|
||
assert calls["calls"][0]["delivery_state"] == "missing"
|
||
assert budget.窗口余额()["未知调用数"] == 0
|
||
command_file = tmp_path / "settlement.json"
|
||
command_file.write_text(json.dumps({"call_id": "call-for-settlement", **body}))
|
||
cli = subprocess.run(
|
||
[
|
||
sys.executable,
|
||
"-m",
|
||
"muse",
|
||
"任务",
|
||
str(file),
|
||
"结算",
|
||
task_id,
|
||
"--额度文件",
|
||
str(command_file),
|
||
],
|
||
cwd="/",
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=20,
|
||
)
|
||
assert cli.returncode == 0, cli.stderr
|
||
assert json.loads(cli.stdout) == response.json()
|