fix(scope-b): 修评审 4 HIGH + 同步 T6 工具数(15)
1. T6 e2e 工具数同步:list 断言 11→15(含 continue/expand/de-ai/teardown), 测试名 ...eleven→fifteen;保留 legacy/ingestable 意图。 2. teardown 书名(body.kind)前置入原文 → 进 text_input context(拆书 prompt 期待书名)。 3. text_input 工具(扩写/降AI/拆书)空原文 → 422 VALIDATION,不让 LLM 空跑。 4. 链 UI:busy 守卫加 awaiting,裁决期间禁止重复发起链。 测试:扩 teardown 断言书名入 context + 新增 expand 空原文 422;后端 642 passed, 前端 294 passed,两门禁全绿。
This commit is contained in:
@@ -492,6 +492,31 @@ async def test_generate_teardown_returns_structured() -> None:
|
|||||||
assert body["output_kind"] == "BookTeardownResult"
|
assert body["output_kind"] == "BookTeardownResult"
|
||||||
assert body["preview"]["themes"] == ["逆袭"]
|
assert body["preview"]["themes"] == ["逆袭"]
|
||||||
assert body["preview"]["structure"] == "黄金三章立钩"
|
assert body["preview"]["structure"] == "黄金三章立钩"
|
||||||
|
# 书名(body.kind)前置入原文 → 进了喂网关的 context(拆书 prompt 期待书名)。
|
||||||
|
assert gateway.last_input is not None
|
||||||
|
assert "书名:某爆款" in gateway.last_input
|
||||||
|
assert "样本章节正文" in gateway.last_input
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_generate_text_input_empty_text_422() -> None:
|
||||||
|
# text_input 工具(扩写/降AI/拆书)原文为空/空白 → 422,不触达网关(不让 LLM 空跑)。
|
||||||
|
repo = FakeProjectRepo()
|
||||||
|
pid = await _seed_project(repo)
|
||||||
|
gateway = _CaptureGateway(PolishResult(text="不应被调用。"))
|
||||||
|
app, session, *_ = _make_app(project_repo=repo, gateway=gateway)
|
||||||
|
|
||||||
|
async with _client(app) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"/projects/{pid}/skills/expand/generate",
|
||||||
|
json={"text": " ", "brief": "加强描写"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 422
|
||||||
|
assert resp.json()["error"]["code"] == ErrorCode.VALIDATION
|
||||||
|
# 校验在生成前拦下:网关未被调用、未 commit。
|
||||||
|
assert gateway.last_input is None
|
||||||
|
assert session.commits == 0
|
||||||
|
|
||||||
|
|
||||||
# ---- POST .../ingest(world_entities gate)----
|
# ---- POST .../ingest(world_entities gate)----
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ async def generate_with_tool(
|
|||||||
world_context = ""
|
world_context = ""
|
||||||
beats: list[str] = []
|
beats: list[str] = []
|
||||||
prior_text = ""
|
prior_text = ""
|
||||||
|
text = body.text or ""
|
||||||
if tool.context_strategy == "with_world":
|
if tool.context_strategy == "with_world":
|
||||||
world_context = await _world_context(memory, project_id)
|
world_context = await _world_context(memory, project_id)
|
||||||
elif tool.context_strategy == "with_outline_chapter":
|
elif tool.context_strategy == "with_outline_chapter":
|
||||||
@@ -202,6 +203,18 @@ async def generate_with_tool(
|
|||||||
chapter_no = body.chapter_no or 1
|
chapter_no = body.chapter_no or 1
|
||||||
prior_text = await _prior_chapter_text(chapter_repo, project_id, chapter_no)
|
prior_text = await _prior_chapter_text(chapter_repo, project_id, chapter_no)
|
||||||
beats = await _chapter_beats(outline_repo, project_id, chapter_no)
|
beats = await _chapter_beats(outline_repo, project_id, chapter_no)
|
||||||
|
elif tool.context_strategy == "text_input":
|
||||||
|
# text_input 工具(扩写/降AI/拆书)必须有原文样本——空原文会让 LLM 空跑(守输入校验纪律)。
|
||||||
|
if not text.strip():
|
||||||
|
raise AppError(
|
||||||
|
ErrorCode.VALIDATION,
|
||||||
|
f"工具 {tool_key} 需要原文输入(text 不可为空)",
|
||||||
|
{"tool_key": tool_key},
|
||||||
|
)
|
||||||
|
# 拆书的书名(body.kind)前置入原文,供拆解对照(teardown system prompt 期待书名)。
|
||||||
|
kind = (body.kind or "").strip()
|
||||||
|
if kind:
|
||||||
|
text = f"书名:{kind}\n\n{text}"
|
||||||
|
|
||||||
context = build_toolbox_context(
|
context = build_toolbox_context(
|
||||||
tool.context_strategy,
|
tool.context_strategy,
|
||||||
@@ -211,7 +224,7 @@ async def generate_with_tool(
|
|||||||
chapter_no=body.chapter_no,
|
chapter_no=body.chapter_no,
|
||||||
beats=beats,
|
beats=beats,
|
||||||
prior_text=prior_text,
|
prior_text=prior_text,
|
||||||
text=body.text or "",
|
text=text,
|
||||||
)
|
)
|
||||||
|
|
||||||
gateway = await build_gateway(spec.tier)
|
gateway = await build_gateway(spec.tier)
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ export function ChainPage({ project }: ChainPageProps) {
|
|||||||
|
|
||||||
const showProgress = jobId !== null && result !== null;
|
const showProgress = jobId !== null && result !== null;
|
||||||
// 链运行中/等待裁决时禁止重复发起。
|
// 链运行中/等待裁决时禁止重复发起。
|
||||||
const busy = starting || (jobId !== null && phase === "running");
|
const busy =
|
||||||
|
starting ||
|
||||||
|
(jobId !== null && (phase === "running" || phase === "awaiting"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ ingest 预检调 `build_gateway("analyst")`——故 override **这一个 builde
|
|||||||
`Gateway`(假适配器据 output_schema 分支:生成产物 vs ContinuityReview)即可覆盖两路。
|
`Gateway`(假适配器据 output_schema 分支:生成产物 vs ContinuityReview)即可覆盖两路。
|
||||||
|
|
||||||
用例:
|
用例:
|
||||||
1. `GET /skills/toolbox` 返回全 11 key(legacy 3 带 legacy_route+is_legacy=true;新 8);
|
1. `GET /skills/toolbox` 返回全 15 key(legacy 3 带 legacy_route+is_legacy=true;新 12);
|
||||||
brainstorm/glossary/golden-finger 的 ingestable 标志符合预期。
|
brainstorm/glossary/golden-finger 的 ingestable 标志符合预期。
|
||||||
2. brainstorm `generate` → 200 ToolGeneratePreviewResponse(output_kind + preview.ideas);
|
2. brainstorm `generate` → 200 ToolGeneratePreviewResponse(output_kind + preview.ideas);
|
||||||
断言 **ledger 真落行** 且 **业务表零新增**(world_entities 计数不变)——不变量 #3。
|
断言 **ledger 真落行** 且 **业务表零新增**(world_entities 计数不变)——不变量 #3。
|
||||||
@@ -241,10 +241,10 @@ def _assert_no_secret_leak(payload: object) -> None:
|
|||||||
assert needle not in blob, f"sensitive marker leaked into response: {needle!r}"
|
assert needle not in blob, f"sensitive marker leaked into response: {needle!r}"
|
||||||
|
|
||||||
|
|
||||||
async def test_t6_list_toolbox_returns_all_eleven_tools(
|
async def test_t6_list_toolbox_returns_all_fifteen_tools(
|
||||||
e2e_sm: async_sessionmaker[AsyncSession],
|
e2e_sm: async_sessionmaker[AsyncSession],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""用例 1:GET /skills/toolbox 返回全 11 key;legacy 3 带 legacy_route,新 8 走通用执行器。"""
|
"""用例 1:GET /skills/toolbox 返回全 15 key;legacy 3 带 legacy_route,新 12 走通用执行器。"""
|
||||||
from ww_api.main import create_app
|
from ww_api.main import create_app
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
@@ -256,7 +256,7 @@ async def test_t6_list_toolbox_returns_all_eleven_tools(
|
|||||||
body = resp.json()
|
body = resp.json()
|
||||||
|
|
||||||
tools = {t["key"]: t for t in body["tools"]}
|
tools = {t["key"]: t for t in body["tools"]}
|
||||||
# 全 11 key(legacy 3 + 新 8)。
|
# 全 15 key(legacy 3 + 新 8 + 竞品快赢 4:续写/扩写/降AI率/拆书)。
|
||||||
assert set(tools) == {
|
assert set(tools) == {
|
||||||
"worldbuilding",
|
"worldbuilding",
|
||||||
"character",
|
"character",
|
||||||
@@ -269,6 +269,10 @@ async def test_t6_list_toolbox_returns_all_eleven_tools(
|
|||||||
"glossary",
|
"glossary",
|
||||||
"opening",
|
"opening",
|
||||||
"fine-outline",
|
"fine-outline",
|
||||||
|
"continue",
|
||||||
|
"expand",
|
||||||
|
"de-ai",
|
||||||
|
"teardown",
|
||||||
}
|
}
|
||||||
# legacy(spec=None):is_legacy=true + 带 legacy_route,不可经通用执行入库。
|
# legacy(spec=None):is_legacy=true + 带 legacy_route,不可经通用执行入库。
|
||||||
legacy_keys = {"worldbuilding", "character", "outline"}
|
legacy_keys = {"worldbuilding", "character", "outline"}
|
||||||
|
|||||||
Reference in New Issue
Block a user