状态: 与 Architecture v1.2 对齐的实现期工程守则
适用范围: Python Core、第一方 Plugin、Provider、TypeScript UI 边界、Process Runtime、Async Runtime、End-to-End 请求闭环
Python 基线: Python >= 3.10;正式验证矩阵至少覆盖 3.10 / 3.11 / 3.12
Dev Harness 不是“多写一些测试”,而是把已经冻结的工程原则变成机器可执行的门禁。
它有三个目标:
- 阻止架构腐化。 已冻结的依赖方向、插件隔离、ExecutionPolicy 无 LLM、Runtime 类型、配置边界等,不允许因为临时方便被绕过。
- 证明 Runtime 语义。 异步任务必须可追踪、可取消、可超时、可回收;Process Plugin 必须遵守 stdin/stdout JSON Lines 协议。
- 证明 End-to-End 请求真正闭环。
accepted不是终态;所有请求都必须最终进入明确终态,并且 UI/Transport 断开、Event 丢失、超时、取消、进程崩溃都不能让请求失联。
核心原则:
所有可以可靠机器验证的架构原则,必须进入 Dev Harness;无法可靠机器判断的设计质量问题,进入 Code Review Guideline。
建议把测试分成三层,而不是把所有内容都塞进 backend/tests/dev_harness/。
backend/tests/
├─ dev_harness/ # 静态架构与工程规则
│ ├─ policy.py
│ ├─ test_dependencies.py
│ ├─ test_contracts.py
│ ├─ test_plugins.py
│ ├─ test_structure.py
│ ├─ test_config.py
│ ├─ test_cross_language.py
│ └─ test_async_ownership.py
│
├─ runtime/ # Runtime 行为契约
│ ├─ test_task_supervisor.py
│ ├─ test_task_queue.py
│ ├─ test_deadline.py
│ ├─ test_cancellation.py
│ ├─ test_process_runtime.py
│ ├─ test_shutdown.py
│ └─ test_recovery.py
│
├─ e2e/ # 请求生命周期闭环
│ ├─ test_sync_request.py
│ ├─ test_async_request.py
│ ├─ test_task_chain.py
│ ├─ test_transport_reconnect.py
│ └─ test_failure_paths.py
│
└─ ... # 普通组件/业务测试
统一入口仍然只有:
python scripts/dev_test.pydev_test 是 Harness 的产品接口;具体测试目录只是内部组织方式。
所有静态工程规则集中放在:
backend/tests/dev_harness/policy.py
至少维护:
PLUGIN_CATEGORIES = {
"playback",
"playlist",
"library",
"recommendation",
"analysis",
"conversation",
}
RUNTIME_TYPES = {"builtin", "process"}
ROOT_CONFIG_FILES = {"app.yaml", "plugins.yaml", "providers.yaml"}
GENERIC_DUMP_MODULES = {"utils.py", "helpers.py", "common.py"}
PYTHON_MIN = (3, 10)
VERIFIED_PYTHON_MINORS = ((3, 10), (3, 11), (3, 12))
SOFT_REVIEW_LINES = 350
HARD_MAX_LINES = 500
SIZE_EXCEPTIONS = set()规则修改必须显式 review。新增 Plugin Category、Runtime Type、根配置文件等,必须先修改 Architecture,再修改 Policy,再实现代码。
以下类型属于稳定、重复、跨边界的数据契约,必须建模为明确 Class/Model,不允许以裸 dict[str, Any] 作为公共接口传播:
InputRequest
ValidatedIntent
ResolvedRequest
TaskDescriptor
ResultBinding
ExecutionDecision
CoreResponse
PluginManifest
PluginResult
PluginError
Observation
RequestState
TaskState
TimeBudget / DeadlineContext
CommittedResult
CoreEvent
Track
PlaylistRef
ProviderRef / ProviderStatus
Python 侧优先使用 Pydantic Model;纯内部、无需 schema/序列化的不可变数据可使用 dataclass(frozen=True)。
Harness 要检查:
- 核心 Contract 是否存在且是正式类;
- 公开方法是否使用已定义 Contract,而不是匿名 dict;
- TypeScript 对应类型是否由 Python Contract 生成,而非手写第二份真源;
- 生成文件是否带
DO NOT EDIT标记并通过 stale-check。
局部纯转换仍应使用函数,例如 normalize_track()、match_alias()、build_prompt(),不要用无状态包装类增加认知成本。
必须硬失败:
Plugin A -X-> Plugin B
Provider -X-> Plugin
Persistence -X-> RequestCoordinator
Backend Plugin -X-> UI
ExecutionPolicy -X-> LLMProvider / SemanticResolver
Core -X-> 具体第三方 Plugin 实现
建议通过 AST / import graph 检查,不用字符串 grep 作为主要实现。
错误信息必须可行动,例如:
ARCH001 recommendation plugin imports playback plugin.
Plugins must not call each other directly; use Core Task orchestration.
生产业务代码禁止裸调用:
asyncio.create_task(...)
threading.Thread(...)
ThreadPoolExecutor(...)例外只能存在于明确登记的基础设施文件,例如:
core/task_supervisor.py
infra/blocking_executor.py
plugins/runtime.py
Harness 应维护 allowlist,而不是广泛豁免目录。
Core 默认只有一个主 asyncio event loop。
生产模块禁止任意创建:
asyncio.new_event_loop()
loop.run_until_complete()
asyncio.run()
除应用启动边界或显式测试 fixture 外。
所有 Builtin Plugin 必须属于以下六类之一:
PlaybackPlugin
PlaylistPlugin
LibraryPlugin
RecommendationPlugin
AnalysisPlugin
ConversationPlugin
Manifest 最小字段必须满足:
api_version
plugin.id
plugin.version
plugin.category
runtime.type
capabilities[].id
runtime.type 只能是:
builtin
process
process 必须提供非空、完整的 runtime.command: list[str]。
Process Plugin:
stdout只能输出 JSON Lines 协议消息;- 日志必须写
stderr; - stdout 中出现非 JSON 协议行视为协议错误;
- Plugin hello 返回的
plugin_id/api_version/plugin_version必须与 Manifest 一致。
Core 根配置只允许:
config/app.yaml
config/plugins.yaml
config/providers.yaml
Plugin 自身 Manifest 跟随插件;用户安装/启停状态只写 plugins.yaml,不得回写插件 Manifest。
<= 350 lines 正常
351-500 lines Harness warning / architecture review
> 500 lines FAIL
合理例外必须登记在 SIZE_EXCEPTIONS,并附注原因;禁止默默放宽全局上限。
任何后台任务必须登记:
request_id
task_id
asyncio.Task handle
created_at
deadline
cancel entry
Runtime test 必须证明:
- supervisor 创建后能查询 task;
- task 完成后自动移出 active 集合并保留终态;
- task 异常不会产生 “Task exception was never retrieved”;
- cancellation 会进入明确 TaskState;
- shutdown 能等待/取消所有登记任务;
- 无 supervisor 持有的 fire-and-forget task 会被 Harness 拦截。
至少测试:
max_queue_size
max_concurrency
per_plugin_concurrency
interactive > background priority
dedup
QUEUE_FULL backpressure
不要只测试“能 enqueue”;必须证明达到容量后会拒绝,并且不会无限增长。
每个 Process Plugin 实例默认:
active_invocation <= 1
第二个调用必须由 CoreTaskQueue 排队,而不是同时向同一 stdin 写多条并发 invocation。
超时计算不能基于 wall clock。
推荐 Runtime 提供统一 Clock 抽象:
Clock.monotonic()
Clock.now_utc() # 仅记录展示/持久化时间
测试必须使用 FakeClock/ManualClock,避免真实 sleep(10) 这类慢且不稳定的测试。
至少包含:
request_deadline
queue_timeout
execution_timeout
process_start_timeout
handshake_timeout
cancel_grace_period
process_idle_timeout
shutdown_grace_period
TaskState 至少记录:
created_at
queued_at
started_at
finished_at
deadline_at
必须有 Harness 明确验证:
request deadline = D
Task A consumes 3s
Task B receives remaining(D - elapsed)
Provider/LLM retry 同样必须消费剩余时间,禁止:
retry #1 timeout=30s
retry #2 又 timeout=30s
retry #3 又 timeout=30s
如果剩余预算不足以开始下一次 retry,立即结束为 timeout/failure。
必须分别测试:
- 任务排队过久 ->
TIMED_OUT,插件从未启动; - 已开始执行但超时 -> 执行取消路径;
- queue timeout 不能偷用 execution timeout;
- execution timeout 在
started_at后才开始计算。
线程只允许解决短时、阻塞、无法原生 async 的调用。
统一由 Core BoundedBlockingExecutor 管理;插件禁止自行创建线程池。
必须测试:
max_workers 固定且来自配置
超过 worker 数的调用排队而不是创建新线程
取消请求不会伪装成“线程已被强杀”
晚到线程结果在 task 已 CANCELLED/TIMED_OUT 时被丢弃
原则:
Python 线程不能可靠强杀,所以任何需要强制 timeout/cancel 的长任务都必须迁移到
processruntime。
Process Runtime 必须用真实测试子进程验证,不用纯 mock 替代。
测试 fixture 建议:
backend/tests/fixtures/process_echo_plugin.py
backend/tests/fixtures/process_slow_plugin.py
backend/tests/fixtures/process_crash_plugin.py
backend/tests/fixtures/process_bad_stdout_plugin.py
覆盖:
spawn
hello handshake
invoke/result
progress(optional)
cancel
shutdown
process_start_timeout
handshake_timeout
execution_timeout
crash detection
bad stdout protocol
stderr logging
idle shutdown
restart on next invocation
强制取消顺序:
cancel message
-> wait cancel_grace_period
-> terminate
-> wait short grace
-> kill if still alive
Process 超时或取消后晚到结果必须被丢弃,禁止将终态从 TIMED_OUT/CANCELLED 改回 COMPLETED。
CoreResponse(status=accepted, terminal=false) 只是 ACK。
Request 必须最终进入以下终态之一:
COMPLETED
FAILED
CANCELLED
TIMED_OUT
REJECTED
INTERRUPTED
Task 也必须有自己的终态,不得只靠 Event 推断。
Harness 要验证非法状态跳转,例如:
COMPLETED -> RUNNING 禁止
CANCELLED -> COMPLETED 禁止
TIMED_OUT -> COMPLETED 禁止
采用 Architecture v1.2 的方案 C:
PluginResult
-> ResultValidator
-> StateCoordinator
-> RuntimeStateStore / SQLite
-> CommittedResult
-> CoreEvent
-> CoreResponse / UI notification
必须证明:
- 状态提交早于
completed响应; - Event 只通知已经提交的事实;
- Event 发送失败不能回滚已经确认的业务终态;
- UI 收到完成事件时,查询 RuntimeStateStore/SQLite 必须已经得到新状态;
- Plugin 不能直接写全局 RuntimeStateStore。
固定验收链:
“暂停”
-> IntentRouter
-> ContextResolver
-> TaskDescriptor
-> ExecutionPolicy
-> PlaybackPlugin(fake/builtin)
-> PluginResult
-> StateCoordinator
-> CoreResponse(completed, terminal=true)
必须断言 request_id/task_id 全程一致,且状态提交早于最终响应。
request
-> RequestState.CREATED
-> queue
-> CoreResponse(accepted, terminal=false)
-> TaskSupervisor owns task
-> process spawn/handshake
-> invoke/result
-> StateCoordinator commit
-> TaskState.COMPLETED
-> RequestState.COMPLETED
-> completion Event
至少覆盖:
queue timeout
execution timeout
user cancellation
process crash
queue full/backpressure
application shutdown
provider retry exhausted
ResultBinding failure
plugin invalid result
每一种都必须产生可查询的最终 RequestState/CoreResponse 语义,禁止 silent drop。
异步任务 ownership 属于 Core,不属于某个 WebSocket/HTTP connection。
Harness 必须模拟:
request -> accepted
transport disconnect
background task completes
transport reconnect
query request_id
重连后必须能够取得最终状态/结果。
断线默认不取消 accepted task;只有显式用户 cancellation 才取消。
事件不是事实源。
异步任务完成顺序必须是:
persist terminal RequestState/TaskState + result
-> commit
-> emit completion event
Harness 应故意让 EventBus 发布失败,然后验证:
get_request(request_id)
仍能返回完整终态。
关闭必须是受控流程:
stop accepting new external requests
-> reject/cancel queued work as policy defines
-> request cancellation of running tasks
-> wait shutdown_grace_period
-> terminate/kill remaining process workers
-> persist terminal/interrupted state
-> flush storage
-> close event loop/resources
必须测试:
- shutdown 后没有未回收 TaskSupervisor task;
- 没有遗留 Process child;
- queued task 不会在 shutdown 后突然启动;
- 非终态记录得到明确
CANCELLED或INTERRUPTED; - shutdown 超时不会永久阻塞应用退出。
V1 不自动恢复未完成任务。
应用启动时:
SQLite 中仍为 QUEUED/RUNNING/CANCELLING 的旧 Request/Task
-> 标记 INTERRUPTED
Harness 必须用预置数据库记录验证该行为。
禁止在没有完整可恢复执行上下文的情况下“猜测继续执行”。
跨插件合作只允许 Core TaskChain。
上一步数据进入下一步必须由 ResultBinding 显式描述,例如:
task_1.data.tracks[0]
-> task_2.input.track
必须测试:
- binding 成功;
- source path 不存在 -> chain FAILED;
- schema 不兼容 -> chain FAILED;
- Task A timeout/cancel/fail -> Task B 不启动;
- 整个 chain 共享剩余 request deadline;
- Recommendation 完成不会因为存在 PlaybackPlugin 而隐式播放。
公共 LLM Provider 支持:
商业 API Key 模式
endpoint + model 模式
Harness 重点不测试真实商业 API,而测试 Core 约束:
- Plugin 不读取 API Key 配置;
- Plugin 只通过公共 Provider 接口调用模型;
- Provider 接受剩余 deadline;
- retry 不刷新 deadline;
- provider timeout 映射到明确 Task failure/timeout;
- endpoint/model 配置模型可在 Python 3.10/3.11/3.12 正确解析。
真实 Provider API 使用契约测试/可选 integration profile,不进入默认快速 dev-test。
pyproject.toml 必须声明:
requires-python = ">=3.10"正式 Harness/CI 至少执行:
Python 3.10
Python 3.11
Python 3.12
必须在每个版本运行的核心集合:
contracts
plugin manifest/runtime
TaskSupervisor/CoreTaskQueue
Process Runtime
StateCoordinator
sync E2E
async E2E
TaskChain/deadline tests
本地如果只安装一个 Python 版本,scripts/dev_test.py 运行当前解释器完整门禁;CI 再负责三版本矩阵。
更新 Python 版本不是禁止的,但未经矩阵验证的版本不称为“正式验证版本”。
TypeScript UI 只通过一个 Core transport facade 访问后端:
frontend/src/transport/core-client.ts
禁止 feature/component 直接拼 Python HTTP/WebSocket/IPC 调用。
Python Contract 为唯一 Source of Truth:
Python Pydantic Model
-> JSON Schema / generator
-> frontend/src/types/generated/contracts.ts
Harness 必须检查生成文件是否 stale。
TypeScript 自身继续使用:
tsc
ESLint
frontend unit tests
不要再手写一套重复架构规则。
推荐 scripts/dev_test.py 依次执行:
1. architecture static harness
2. Python contract/component tests
3. async/runtime tests
4. E2E lifecycle tests
5. Python compile/static checks
6. generated TypeScript contract stale-check
7. TypeScript typecheck
8. ESLint / frontend unit tests(前端开始实现后启用)
任何一步失败立即停止,并显示具体 Harness rule ID。
本地快速模式可支持:
python scripts/dev_test.py --fast
但 --fast 只能跳过明确的慢 integration profile,不能跳过架构 Harness、核心 Runtime、E2E 生命周期测试。
统一使用 FakeClock/ManualClock 或可注入 monotonic clock。
IPC、cancel、stdout protocol、crash 等必须使用 fixture process;mock 无法证明真正的进程边界。
Fake 的只是外部副作用,不是假掉:
Intent -> Context -> Task -> Execution -> PluginManager -> StateCoordinator -> CoreResponse
测试名应该能直接说明架构要求,例如:
test_accepted_async_request_eventually_reaches_terminal_state
test_cancelled_task_rejects_late_plugin_result
test_provider_retry_does_not_reset_deadline
test_state_is_committed_before_completion_event
test_transport_disconnect_does_not_cancel_accepted_task
任何会改变架构边界的开发遵循:
Architecture decision
-> update Harness rule/test first
-> verify RED
-> implement minimum change
-> verify GREEN
-> refactor
-> run full dev_test
例如未来新增一个 Plugin Category:
- 先更新 Architecture;
- 修改
PLUGIN_CATEGORIES; - 写该类别合法/非法行为测试;
- 运行红测;
- 实现 Base Category Class/Manifest;
- 全量 dev-test。
禁止先改业务代码,再为了让 CI 通过而事后放宽 Harness。
以下内容不建议写成脆弱测试,而应在 review 中检查:
- 类是否真的表达稳定语义,而不是无意义包装;
- 模块命名是否准确;
- 抽象是否过早;
- Plugin 是否承担了不属于自己的业务责任;
- Prompt 是否应该拆分;
- 某文件 300 行是否已经难读,即使没有触发 500 行硬限制;
- 是否出现不必要的新依赖;
- 是否有更简单的实现能保持同样契约。
Harness 用于保护确定边界,Code Review 用于保护设计质量。
在进入真实业务插件开发前,至少保证以下门禁全部存在并转绿:
ARCH-01 Plugin dependency isolation
ARCH-02 ExecutionPolicy has no LLM dependency
ARCH-03 six frozen plugin categories only
ARCH-04 runtime only builtin/process
ARCH-05 process command is complete list[str]
ARCH-06 contract models are classes/models
ARCH-07 no generic dump modules
ARCH-08 file-size budget
ARCH-09 Python -> TypeScript contract single source
ARCH-10 no bare fire-and-forget task/thread
ASYNC-01 TaskSupervisor owns every background coroutine
ASYNC-02 bounded queue and backpressure
ASYNC-03 single active invocation per process plugin instance
ASYNC-04 queue timeout
ASYNC-05 execution timeout
ASYNC-06 remaining-deadline propagation
ASYNC-07 provider retry does not reset deadline
ASYNC-08 cancellation and late-result rejection
ASYNC-09 controlled shutdown
ASYNC-10 interrupted-state recovery
E2E-01 sync playback fast path closes with CoreResponse
E2E-02 async process request accepted -> terminal
E2E-03 transport reconnect can query terminal result
E2E-04 event loss does not lose terminal state
E2E-05 TaskChain uses explicit ResultBinding
E2E-06 chain failure prevents downstream execution
COMPAT-01 Python >=3.10 metadata
COMPAT-02 verified matrix 3.10/3.11/3.12
这些测试不是为了追求覆盖率数字,而是为了保证已经冻结的 Architecture v1.2 在后续开发中不会逐渐失真。
Harness 需要长期坚持四条纪律:
- Contract first。 稳定模板数据先建模,再传递。
- Ownership explicit。 task、thread、process、state 都必须有明确 Core owner。
- Deadline monotonic。 时间预算只能消耗,不能在内部重试或 TaskChain 中偷偷刷新。
- Terminal state authoritative。 Event/UI 只是通知面;RequestState/TaskState + committed result 才是异步请求的权威事实。
只要这四条和统一 dev_test 一直成立,后续 Plugin 数量增长、LLM Provider 增加、Analysis/Recommendation 复杂化,都不会轻易破坏 Core 的可维护性。