关于 propagate
propagate 是根据 Allium 规格为目标代码库生成测试的技能,涵盖边界契约和全规格义务,并支持断言、基于属性及状态机测试。
主要功能
- 01根据规格义务生成测试
- 02从 surface 声明生成边界测试
- 03覆盖完整规格的测试义务
- 04支持断言、属性与状态机测试
- 05关联规格构造与实现代码
适用场景
- 01测试 API、UI 或集成边界
- 02覆盖规则、实体和状态转换
- 03用基于属性的测试检查不变量
- 04验证实体的完整生命周期
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ juxt/ allium --skill propagate安装后在 Claude Code 中输入/propagate确认已加载。
详细介绍
Propagation
此技能会根据 Allium 规范生成测试。Propagation 指植物通过母株的插枝繁殖:规范是母株,测试是后代。
确定性工具可确保完整性(每个规范构造都会映射到一项测试义务)。你负责打通实现环节:将规范构造与代码关联起来,并按照项目约定生成测试。
交互模式
此技能以两种模式运行。以下所有要求或建议用户执行操作的指示,均遵循相应模式:
- 交互模式 — 在对话中内联运行。直接询问用户并等待答复。
- 非交互模式 — 作为
propagate子 Agent 运行(例如在 Allium 循环中运行),无法联系用户。对于需要人工决策的事项——规范过于粗略而无法传播、实现环节含糊不清、未覆盖的义务——在最终输出中报告,并继续处理不依赖这些事项的工作。
前置条件
生成测试之前,需要:
- Allium 规范 — 描述系统行为的
.allium文件 - 目标代码库 — 要测试的实现
- 测试义务 — 通过
allium plan <spec>获取(列出所有必需测试的 JSON) - 领域模型 — 通过
allium model <spec>获取(描述实体结构、约束和状态机的 JSON)
如果 CLI 工具不可用,请根据规范,使用 references/test-generation.md 中的测试生成分类手动推导测试义务。
模式
表层模式
根据表层声明生成边界测试。适用于用户希望测试 API、UI 契约或集成边界的情况。
对于规范中的每个表层:
- 暴露测试 — 验证
exposes中的每个项目都可供指定的参与者访问,包括对集合使用for迭代 - 提供项测试 — 验证当
when条件为真时,操作会显示;否则会隐藏,包括相应规则的requires子句未满足时 - 参与者限制测试 — 验证其他参与者类型无法访问该表层
- 参与者识别测试 — 验证只有符合参与者
identified_by谓词的实体才能交互;对于带有within的参与者,验证交互范围限定在声明的上下文中 - 上下文范围测试 — 验证当没有实体符合
context谓词时,表层实例不会出现 - 契约义务测试 — 验证对端满足
demands,此表层提供fulfils,包括所有带类型的签名 - 保证测试 — 验证
@guarantee注解在边界范围内均成立 - 超时测试 — 验证被引用的时间规则会在表层上下文中触发
- 相关导航测试 — 验证导航到相关表层时,会解析到正确的上下文实体
规范模式
遍历完整的测试义务文档。当用户希望针对整个规范获得全面的测试覆盖时使用。
测试生成分类法中的类别:
- 实体和值类型测试 — 字段、类型、可选项(
?)的空值处理、when子句中依状态决定是否存在、关系、联接查询、相等性 - 枚举测试 — 命名枚举之间的可比性、成员测试、内联枚举隔离
- 和类型测试 — 变体字段、类型守卫、穷尽性、通过变体名称创建、基础
.created触发器收窄 - 派生值和投影测试 — 计算、筛选、
-> field提取、参数化派生值、now易变性、集合操作 - 默认实例测试 — 无条件存在、字段值、默认值之间的交叉引用
- 配置测试 — 默认值、覆盖值、必需参数、表达式形式的默认值、限定引用、配置链
- 不变量测试 — 规则执行后的验证、边缘情况、蕴含逻辑、实体级不变量
- 规则测试 — 成功/失败/边缘情况、条件语句(确保
if守卫读取结果状态)、实体创建、移除、批量更新、规则级for迭代、let绑定、链式触发器 - 状态转换测试 — 有效/无效转换、终止状态、
transitions_to与becomes的语义 - 时序测试 — 截止时间边界、防止重复触发、可选字段的空值行为
- 表层测试 — 暴露、可用性、通过
within作用域识别参与者、上下文作用域、相关导航 - 契约测试 — 签名满足、遵守
@invariant、demands/fulfils的方向 - 跨模块测试 — 限定实体引用、外部触发器响应、类型占位符替换
- 跨规则交互测试 — 重复创建守卫、
provides可用性 - 转换图测试 — 每条已声明的边都能通过见证规则到达、拒绝未声明的转换、终止状态没有出边规则、非终止状态至少有一个出口、枚举值与图边精确对应
- 依赖状态的字段测试 — 处于符合条件的状态时存在、处于其他状态时不存在、进入
when集合时的存在义务、离开集合时的缺失义务、在集合内部或外部移动时无义务、所有汇合转换都设置字段、访问when限定字段所需的守卫、通过输入交集推断派生值的when - 场景测试 — 顺利路径、边缘情况、顺序无关性
- 数据流链测试 — 测试从表层捕获到规则,再到下游规则前置条件的完整链路。对于每条链(表层提供触发器 → 规则确保字段存在 → 下游规则要求该字段),生成集成测试,通过表层提交数据,并验证数据到达下游前置条件。
- 可达性测试 — 从每个初始状态(通过
.created()),沿转换图中的有效路径遍历至每个终止状态。每项测试都覆盖完整生命周期。 - 死锁场景测试 — 对于
allium analyse识别出潜在死锁的状态,生成测试,将实体置于卡死状态并验证其是否能够继续推进。 - 跨实体流程测试 — 对于跨多个实体的流程,生成集成测试,覆盖所有参与实体从起始到终止状态的完整流程。
如果 allium analyse 可用,请利用其发现结果确定测试生成的优先级。missing_producer 或 dead_transition 发现结果表示存在值得通过测试覆盖的缺口。deadlock 发现结果应生成一个测试,记录该实体无法脱离卡死状态。请参阅处理发现结果,了解发现类型的分类法。
测试输出类型
1. 基于断言的测试
适用于确定性义务:字段是否存在、枚举成员资格、转换有效性、界面暴露情况,以及依赖状态的字段是否存在或缺失。这些都是标准的单元测试/集成测试。
2. 基于属性的测试
适用于不变量和规则属性。每个包含表达式的不变量都会成为一个 PBT 属性:
- 使用生成器规范生成有效的实体状态
- 应用一系列规则(若已声明转换图,则遵循该图;否则仅根据规则推导有效序列)
- 检查每一步是否都满足不变量
使用项目的 PBT 框架:
| 语言 | 框架 | 查找位置 |
|---|---|---|
| TypeScript | fast-check | package.json |
| Python | Hypothesis | pyproject.toml |
| Rust | proptest | Cargo.toml |
| Go | rapid | go.mod |
| Elixir | StreamData | mix.exs |
如果项目中没有 PBT 框架,则回退到基于断言的测试。
3. 状态机测试
适用于具有状态枚举的实体。若已声明转换图,则遍历图中的每条路径。若未声明转换图,则根据规则推导有效转换。
- 验证通过见证规则可以成功进行转换
- 验证被拒绝的转换会失败
- 根据各状态的
when子句,验证依赖状态的字段在每个状态下存在或缺失 - 验证每个状态下都满足不变量
状态机测试需要动作映射:针对每条转换边都有一个函数,该函数接收处于源状态的实体,并通过调用实际实现代码将其转换为目标状态。没有这个映射,测试框架可以描述图中的有效路径,但无法执行这些路径。
构建动作映射:
- 对于转换图中的每条边,在规范中查找作为依据的规则
- 查找实现该规则的代码(实现桥接)
- 编写一个测试动作,用于设置前置条件(
requires子句)、调用代码,并返回处于目标状态的实体 - 使用
(from_state, to_state)键注册该动作
构建映射后,PBT 框架便可遍历随机的有效路径:从任意非终止状态开始,随机选择一条出边,执行其动作,检查所有实体级不变量,然后重复。路径长度和起始状态都是随机生成的。这是对规范中转换图最完整的测试形式。
实现桥接
你需要将规范中的构造与实现代码关联起来,方式与 weed 技能进行差异检查时相同。
表面测试
将各个界面映射到其实现:
- API 界面映射到端点(REST 路由、GraphQL 解析器、gRPC 服务)
- UI 界面映射到组件或页面
- 集成界面映射到消息处理器或 SDK 方法
通过阅读代码库来发现映射关系。查找命名模式、路由定义和处理器注册。
内部测试
对于规范中的每条规则:
- 查找实现该规则的代码(服务方法、事件处理器、状态机转换)
- 确定如何实例化涉及的实体(工厂、构建器、测试固件)
- 确定如何调用该规则(API 调用、方法调用、事件分发)
- 确定如何断言后置条件(数据库查询、返回值、事件断言)
对于时间测试
基于时间的触发器(基于截止时间的规则)需要在测试中使用可控的时间源。如果实现使用墙上时钟时间(Instant.now()、System.currentTimeMillis()),测试就无法可靠地将时间定位在截止时间之前、到达截止时间时或之后。
在尝试进行时间测试之前,请检查组件是否接受注入的时钟或时间参数。常见模式包括:构造函数中的 Clock 参数、方法的纪元毫秒参数、TimeProvider 接口。如果存在这样的注入接口,请注入可控的时间源。如果不存在,请将其标记为测试基础设施缺口:在组件支持时间注入之前,无法生成时间测试。不要尝试通过休眠或与墙上时钟时间竞争来测试时间行为。
对于跨模块触发器链
当一条规则发出的触发器由另一份规范中的规则接收时(例如,Arbiter 发出 ClerkReceivesEvent,Clerk 负责处理),测试这条链需要将多个组件连接在一起。
生成跨模块测试之前:
- 根据计划输出追踪触发器发射图:哪些规则发出触发器,其他规范中的哪些规则接收这些触发器
- 检查代码库中是否已有连接参与组件的集成测试夹具(流水线测试、端到端测试辅助工具、测试框架类)
- 如果存在夹具,请复用它。跨模块测试应组合现有的连接方式,而不是重新构建
- 如果没有夹具,但代码库结构足够清晰,可以理解连接方式(服务构造函数、依赖注入、事件总线配置),则生成夹具和测试
- 如果连接方式过于复杂或不透明,无法有把握地生成,则生成测试骨架,并使用 TODO 标记需要连接组件的位置
跨模块测试本质上是集成测试。它们用于验证规范中的触发器链是否在组件边界之间得到忠实实现。单组件测试通过后,再优先处理这些测试。
复用现有测试
探索代码库时,请记录现有测试已经覆盖了哪些规范义务。一个覆盖了从提交事件到输出确认的成功路径的集成测试,已经覆盖了多个 rule_success 义务和端到端场景。
当现有测试覆盖某项规范义务时,请引用该测试,而不是生成重复测试。propagate 技能在集成层面的价值在于,根据规范义务列表验证覆盖是否完整、识别缺口,并生成测试来填补缺口——下方流程中的核对步骤会明确执行这一检查。用生成的等效测试替换有效的手写测试毫无价值。
对于延迟规范
延迟规范在单独的文件中有完整说明。当目标代码库不包含延迟规范对应的模块时,请生成一个带占位符的测试存根:
// TODO: deferred spec — InterviewerMatching.suggest
// This behaviour is specified as deferred. Provide a mock or skip.
流程
- 阅读规范 — 理解实体、规则、界面、不变量、转换图、依赖状态的字段、契约、配置和默认值。阅读评估规范,判断规范的成熟度。粗略的规范(包含实体和转换图,但没有规则)只能产生有限的测试义务——主要是结构测试。如果规范过于粗略,无法有意义地生成测试,请建议先使用
elicit或distill技能进一步完善规范,再生成测试。包含规则和界面的规范支持完整的测试分类,包括数据流链测试和可达性测试。 - 阅读测试义务 — 来源为
allium plan的输出或手动推导 - 阅读领域模型 — 来源为
allium model的输出或手动推导 - 探索代码库 — 查找现有测试、测试框架、实体实现和规则实现
- 将构造映射到代码 — 将规范中的实体、规则和界面与实现类、函数或端点对应起来
- 生成测试 — 按照项目约定生成测试文件
- 验证测试能否编译和运行 — 确保生成的测试在语法上有效
- 核对义务 — 确认每项义务都对应一个覆盖它的测试,并自动补齐任何遗漏(参见义务核对)
探索清单
生成测试前,请先确认:
- 测试框架和运行器(Jest、pytest、cargo test 等)
- 是否存在 PBT 框架(fast-check、Hypothesis、proptest 等)
- 测试文件的位置约定(与源文件同目录、
__tests__/、tests/等) - 实体/模型的位置和模式(类、接口、结构体)
- 测试数据的工厂/夹具模式
- 状态转换的实现方式(方法、事件、状态机)
- 界面的实现方式(路由、控制器、解析器)
- 现有测试辅助工具或实用程序
- 组件是否接受注入的时间源,以便进行时间相关测试
- 是否存在可用于跨模块触发链的集成测试夹具
- 现有测试已覆盖哪些规范义务
生成器注意事项
有生成器规范时,请用它们生成有效的测试数据:
- 遵循字段类型和约束
- 对于包含转换图的实体,生成处于特定生命周期状态的实体,并确保字段存在情况符合
when子句(例如,shipped状态的 Order 已填充tracking_number和shipped_at;pending状态的 Order 则没有) - 对于不变量,生成能测试边界条件的状态
- 对于配置参数,除非在测试覆盖时进行覆盖,否则使用声明的默认值
义务核对
生成测试后,遍历完整的义务列表——来自 allium plan 输出或手动推导——并将每项义务对应到覆盖它的测试,无论该测试是在本次运行中生成还是预先存在。核对过程应保持静默:义务是内部工作数据,因此只要一切正常,就不要叙述对应关系、打印义务列表或提供覆盖报告。
对于没有测试覆盖的每项义务,尝试为其补充覆盖:
- 生成缺失的测试,或扩展已有测试以覆盖其尚未覆盖的部分
- 验证新测试能够编译并运行
- 根据结果重新核对该义务
这是一个小循环,需要使用与外层 Allium 循环相同的保护机制(参见驱动循环):
- 尝试次数上限 — 每项义务最多尝试 2 次。如果第二次尝试仍未能生成一个可编译且覆盖该义务的测试,就暂缓处理该义务。
- 无进展上限 — 如果对剩余缺口完整遍历一轮后没有覆盖任何新内容,就停止;继续遍历只会徒劳循环。
触发保护机制后仍未覆盖的义务,是核对结果中唯一需要让用户看到的部分。逐项报告,并附上分类和一句原因:
- 基础设施缺口 — 测试需要代码未提供的接入点:时间相关义务没有可注入的时钟,跨模块调用链没有集成测试夹具
- 无法映射的构造 — 在代码优先流程中,没有实现代码与规范构造相对应;无法建立实现桥接
实现缺失不是残留项类别。在规范优先流程中,尚无代码是设计如此:针对预期接口编写测试并让它失败——失败的测试也覆盖其义务,循环的实现阶段会使其通过。只有完全无法编写任何测试时,义务才算未覆盖。
最后用一行摘要收尾:N obligations, M covered, K uncovered。全部义务都已覆盖时,这一行就是核对过程面向用户的全部输出。没有单独列出某项义务,表示该义务已覆盖;凡是逐项列出的内容,都需要人工决策。当 propagate 在 Allium 循环中运行时,这一行会并入循环的汇总摘要;只要仍有未覆盖且没有报告原因的义务,循环就不得将规范视为已收敛。
记录篡改基线(在循环中)
在 Allium 循环中运行时,记录核对基线,以便循环的独立验证者之后确认没有内容被伪造。在账本(.allium-loop/<goal-slug>.json,参见驱动循环)中:
- 为每个生成的测试文件计算内容哈希(例如 sha256),按路径作为键记录在
generated_test_hashes下; - 在
reconciliation下记录核对摘要行。
哈希是 witness 技能重新推导出的基准事实:如果某个生成的测试哈希发生变化,期间却没有运行 propagate,那就是手动编辑的测试——循环绝不能在这种违反反作弊规则的情况下达到收敛。记录基线才能进行这项检查;如果跳过,验证者可以确认测试通过,却无法确认测试未被弱化。这只是简单的记录工作,不是报告——不要叙述此事。
类型化结果(循环交接)
在 Allium 循环中作为 propagate 子 Agent 运行时,请将结果作为单个符合 propagate-result.schema.json 的 JSON 对象返回,不要附加其他内容。循环根据结构化字段进行路由:obligations 和 uncovered_obligations 决定覆盖是否完整(只要 uncovered_obligations 非空,循环就不得收敛),而 generated_tests 则包含 witness 会重新推导的路径与哈希基线——即上文记录的相同哈希,现在作为交接内容中的一等字段。summary 字段请保持为 N obligations, M covered, K uncovered 这一行。请输出每个字段,空列表使用 [],不要省略字段。交互运行时,摘要行仍与之前一样,是面向用户的全部输出;类型化记录用于机器交接,而非对话。
{
"phase": "propagate",
"obligations": { "total": 12, "covered": 12, "uncovered": 0 },
"uncovered_obligations": [],
"generated_tests": [{ "path": "order.test.js", "hash": "sha256:9f2c…" }],
"test_paths": ["order.test.js"],
"open_questions": [],
"summary": "12 obligations, 12 covered, 0 uncovered"
}
与其他工具的交互
- distill 根据代码生成规格。这些规格会传给 propagate。
- weed 检查一致性。测试传播后,weed 会验证规格与代码是否匹配。
- tend 演进规格。规格变更后,再次运行 propagate 以更新测试。
- elicit 通过对话构建规格。规格准备就绪后,propagate 会生成测试。
局限性
- 生成的测试只是起点,可能需要根据项目特定模式进行调整。
- 实现桥接由大模型驱动。复杂或不常见的代码库可能需要手动指导映射过程。
- 跨模块测试需要理解服务边界之间的组件连接方式。当代码库结构清晰时,可以生成完整测试。当连接方式不明确时,生成的测试会是骨架,并包含供手动配置的 TODO。
- 运行时跟踪验证和模型检查属于独立的工作流。