Files
writer-work-flow/packages/agents/tests/test_competitor_specs.py
Yaojia Wang dca4d45d4e refactor(agents): prompt 外置方案A — 21 prompt 散文外迁 .md + SpecResolver
把 21 个内置 agent 的 system_prompt 从 specs.py 的 Python 常量外置为
prompts/<spec.name>.md,import 期由 load_prompt 确定性加载;建立 SPECS 名册
+ SCHEMA_CATALOG(Pydantic 类型留 Python)+ 统一只读解析入口 SpecResolver。
纯重构、零功能/schema 变更,缓存断点前块字节级不变(不变量 #9)。

@llm packages/agents(步骤1-3)
- spec_model.py:抽出 AgentSpec(frozen,字段不变)
- prompt_loader.py:load_prompt = utf-8-sig 去BOM → LF 归一 → NFC → rstrip尾LF,
  内存缓存 + fail-fast(PromptNotFoundError),import 期确定性
- schema_catalog.py:SCHEMA_CATALOG[name]→output type 唯一真相源(refiner=None)
- prompts/*.md ×21:取常量「运行时值」程序化外迁(反斜杠折行已塌缩,
  物理换行≡运行时换行);文件名按 spec.name 连字符(style.md/character-gen.md 等)
- specs.py:删 21 常量 + AgentSpec 类;system_prompt=load_prompt(name)、
  output_schema=SCHEMA_CATALOG[name];建 SPECS + REVIEW_RESERVED_NAMES;
  *_spec 兼容期保留且 SPECS[name] is *_spec(同一实例)。804→337 行
- __init__.py:显式 __all__ 重导出(避 F401)

@backend packages/skills(步骤4-5)
- SpecResolver:内置 SPECS(纯内存、零 DB)+ 用户 SkillRegistry 统一 get;
  内置 name 永不触发 DB;output_schema_for 精确匹配
- skill_registry:保留命名空间守卫前移至入库校验,拒同名内置 → VALIDATION
- toolbox_registry:GeneratorTool.spec 改走 SPECS[...],删 12 个 *_spec 直接 import

@devops repo-root
- .gitattributes:prompts/*.md text eol=lf(修正:须用完整嵌套路径才匹配)
- packages/agents/pyproject:hatchling artifacts 纳入 prompts/*.md 随 wheel/sdist 分发
- ci.yml:新增 build wheel → 裸装 → import ww_agents.SPECS 冒烟

TDD 全程 mock 网关;门禁绿:ruff/format clean · mypy 209 files · pytest 744 passed
(含金标准 sha256 回归 / md↔spec↔catalog 一一对应 / fail-fast / BOM+NFC /
内置守卫 / 同一实例 / 编排器无回归 / apps/api import-smoke / 打包冒烟)
2026-06-24 04:49:44 +02:00

100 lines
3.3 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.

"""Scope B 竞品快赢 · 4 个生成器的 spec/schema 契约测试。
校验每个 spec 的 name/tier/reads/writes/output_schema/scope/input_schema
对齐不变量 #2只声明 tier/ #3续写/扩写/降AI 纯预览 writes=[];拆书 F1 落库 rules
不联网、无 DB。
"""
from __future__ import annotations
import pytest
from ww_agents import (
AgentSpec,
BookTeardownResult,
ContinuationResult,
DeAiResult,
PolishResult,
continue_spec,
de_ai_spec,
expand_spec,
teardown_spec,
)
# (spec, name, tier, reads, writes, output_schema)
_COMPETITOR_SPECS = [
(
continue_spec,
"continue",
"writer",
["projects", "outline", "chapters"],
[],
ContinuationResult,
),
(expand_spec, "expand", "writer", ["projects"], [], PolishResult),
(de_ai_spec, "de-ai", "analyst", ["projects"], [], DeAiResult),
# F1拆书结论可落库为项目 ruleswrites=["rules"];其余三者仍纯预览)。
(teardown_spec, "teardown", "analyst", ["projects"], ["rules"], BookTeardownResult),
]
@pytest.mark.parametrize(
("spec", "name", "tier", "reads", "writes", "output_schema"),
_COMPETITOR_SPECS,
)
def test_competitor_spec_declares_expected_contract(
spec: AgentSpec,
name: str,
tier: str,
reads: list[str],
writes: list[str],
output_schema: type,
) -> None:
assert spec.name == name
assert spec.tier == tier
assert list(spec.reads) == reads
assert list(spec.writes) == writes # 不变量 #3续写/扩写/降AI 纯预览;拆书落 rules
assert spec.output_schema is output_schema
assert spec.scope == "builtin"
assert spec.input_schema is None # 注入材料为序列化文本,非结构化入参
@pytest.mark.parametrize("spec", [row[0] for row in _COMPETITOR_SPECS])
def test_competitor_spec_has_nonempty_system_prompt(spec: AgentSpec) -> None:
assert spec.system_prompt.strip()
# ---- schema 解析/默认值契约 ----
def test_continuation_result_parses_text() -> None:
parsed = ContinuationResult.model_validate({"text": "雷光再起,他迈步向前。"})
assert parsed.text == "雷光再起,他迈步向前。"
def test_polish_result_parses_text() -> None:
parsed = PolishResult.model_validate({"text": "夜色更浓,风穿过断壁。"})
assert parsed.text == "夜色更浓,风穿过断壁。"
def test_de_ai_result_parses_text() -> None:
parsed = DeAiResult.model_validate({"text": "他咧嘴一笑,没多说什么。"})
assert parsed.text == "他咧嘴一笑,没多说什么。"
def test_book_teardown_result_parses_and_lists_default_empty() -> None:
empty = BookTeardownResult(structure="开篇立钩→铺垫→爆发")
assert empty.themes == []
assert empty.archetypes == []
assert empty.hooks == []
parsed = BookTeardownResult.model_validate(
{
"themes": ["逆袭", "复仇"],
"archetypes": ["废柴主角", "腹黑反派"],
"structure": "黄金三章立爽点,逐卷升级",
"hooks": ["章末反转", "扮猪吃虎"],
}
)
assert parsed.themes == ["逆袭", "复仇"]
assert parsed.structure == "黄金三章立爽点,逐卷升级"
assert parsed.hooks == ["章末反转", "扮猪吃虎"]