mcpField
P
propagate

propagate

github.com/juxt测试与质量5242.6k 次安装4 周前更新
代码仓库

关于 propagate

propagate 是根据 Allium 规格为目标代码库生成测试的技能,涵盖边界契约和全规格义务,并支持断言、基于属性及状态机测试。

主要功能

  1. 01根据规格义务生成测试
  2. 02从 surface 声明生成边界测试
  3. 03覆盖完整规格的测试义务
  4. 04支持断言、属性与状态机测试
  5. 05关联规格构造与实现代码

适用场景

  1. 01测试 API、UI 或集成边界
  2. 02覆盖规则、实体和状态转换
  3. 03用基于属性的测试检查不变量
  4. 04验证实体的完整生命周期

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/juxt/allium --skill propagate

安装后在 Claude Code 中输入/propagate确认已加载。

详细介绍

Propagation

此技能会根据 Allium 规范生成测试。Propagation 指植物通过母株的插枝繁殖:规范是母株,测试是后代。

确定性工具可确保完整性(每个规范构造都会映射到一项测试义务)。你负责打通实现环节:将规范构造与代码关联起来,并按照项目约定生成测试。

交互模式

此技能以两种模式运行。以下所有要求或建议用户执行操作的指示,均遵循相应模式:

  • 交互模式 — 在对话中内联运行。直接询问用户并等待答复。
  • 非交互模式 — 作为 propagate 子 Agent 运行(例如在 Allium 循环中运行),无法联系用户。对于需要人工决策的事项——规范过于粗略而无法传播、实现环节含糊不清、未覆盖的义务——在最终输出中报告,并继续处理不依赖这些事项的工作。

前置条件

生成测试之前,需要:

  1. Allium 规范 — 描述系统行为的 .allium 文件
  2. 目标代码库 — 要测试的实现
  3. 测试义务 — 通过 allium plan <spec> 获取(列出所有必需测试的 JSON)
  4. 领域模型 — 通过 allium model <spec> 获取(描述实体结构、约束和状态机的 JSON)

如果 CLI 工具不可用,请根据规范,使用 references/test-generation.md 中的测试生成分类手动推导测试义务。

模式

表层模式

根据表层声明生成边界测试。适用于用户希望测试 API、UI 契约或集成边界的情况。

对于规范中的每个表层:

  1. 暴露测试 — 验证 exposes 中的每个项目都可供指定的参与者访问,包括对集合使用 for 迭代
  2. 提供项测试 — 验证当 when 条件为真时,操作会显示;否则会隐藏,包括相应规则的 requires 子句未满足时
  3. 参与者限制测试 — 验证其他参与者类型无法访问该表层
  4. 参与者识别测试 — 验证只有符合参与者 identified_by 谓词的实体才能交互;对于带有 within 的参与者,验证交互范围限定在声明的上下文中
  5. 上下文范围测试 — 验证当没有实体符合 context 谓词时,表层实例不会出现
  6. 契约义务测试 — 验证对端满足 demands,此表层提供 fulfils,包括所有带类型的签名
  7. 保证测试 — 验证 @guarantee 注解在边界范围内均成立
  8. 超时测试 — 验证被引用的时间规则会在表层上下文中触发
  9. 相关导航测试 — 验证导航到相关表层时,会解析到正确的上下文实体
规范模式

遍历完整的测试义务文档。当用户希望针对整个规范获得全面的测试覆盖时使用。

测试生成分类法中的类别:

  • 实体和值类型测试 — 字段、类型、可选项(?)的空值处理、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 框架:

语言框架查找位置
TypeScriptfast-checkpackage.json
PythonHypothesispyproject.toml
RustproptestCargo.toml
Gorapidgo.mod
ElixirStreamDatamix.exs

如果项目中没有 PBT 框架,则回退到基于断言的测试。

3. 状态机测试

适用于具有状态枚举的实体。若已声明转换图,则遍历图中的每条路径。若未声明转换图,则根据规则推导有效转换。

  • 验证通过见证规则可以成功进行转换
  • 验证被拒绝的转换会失败
  • 根据各状态的 when 子句,验证依赖状态的字段在每个状态下存在或缺失
  • 验证每个状态下都满足不变量

状态机测试需要动作映射:针对每条转换边都有一个函数,该函数接收处于源状态的实体,并通过调用实际实现代码将其转换为目标状态。没有这个映射,测试框架可以描述图中的有效路径,但无法执行这些路径。

构建动作映射:

  1. 对于转换图中的每条边,在规范中查找作为依据的规则
  2. 查找实现该规则的代码(实现桥接)
  3. 编写一个测试动作,用于设置前置条件(requires 子句)、调用代码,并返回处于目标状态的实体
  4. 使用 (from_state, to_state) 键注册该动作

构建映射后,PBT 框架便可遍历随机的有效路径:从任意非终止状态开始,随机选择一条出边,执行其动作,检查所有实体级不变量,然后重复。路径长度和起始状态都是随机生成的。这是对规范中转换图最完整的测试形式。

实现桥接

你需要将规范中的构造与实现代码关联起来,方式与 weed 技能进行差异检查时相同。

表面测试

将各个界面映射到其实现:

  • API 界面映射到端点(REST 路由、GraphQL 解析器、gRPC 服务)
  • UI 界面映射到组件或页面
  • 集成界面映射到消息处理器或 SDK 方法

通过阅读代码库来发现映射关系。查找命名模式、路由定义和处理器注册。

内部测试

对于规范中的每条规则:

  1. 查找实现该规则的代码(服务方法、事件处理器、状态机转换)
  2. 确定如何实例化涉及的实体(工厂、构建器、测试固件)
  3. 确定如何调用该规则(API 调用、方法调用、事件分发)
  4. 确定如何断言后置条件(数据库查询、返回值、事件断言)
对于时间测试

基于时间的触发器(基于截止时间的规则)需要在测试中使用可控的时间源。如果实现使用墙上时钟时间(Instant.now()、System.currentTimeMillis()),测试就无法可靠地将时间定位在截止时间之前、到达截止时间时或之后。

在尝试进行时间测试之前,请检查组件是否接受注入的时钟或时间参数。常见模式包括:构造函数中的 Clock 参数、方法的纪元毫秒参数、TimeProvider 接口。如果存在这样的注入接口,请注入可控的时间源。如果不存在,请将其标记为测试基础设施缺口:在组件支持时间注入之前,无法生成时间测试。不要尝试通过休眠或与墙上时钟时间竞争来测试时间行为。

对于跨模块触发器链

当一条规则发出的触发器由另一份规范中的规则接收时(例如,Arbiter 发出 ClerkReceivesEvent,Clerk 负责处理),测试这条链需要将多个组件连接在一起。

生成跨模块测试之前:

  1. 根据计划输出追踪触发器发射图:哪些规则发出触发器,其他规范中的哪些规则接收这些触发器
  2. 检查代码库中是否已有连接参与组件的集成测试夹具(流水线测试、端到端测试辅助工具、测试框架类)
  3. 如果存在夹具,请复用它。跨模块测试应组合现有的连接方式,而不是重新构建
  4. 如果没有夹具,但代码库结构足够清晰,可以理解连接方式(服务构造函数、依赖注入、事件总线配置),则生成夹具和测试
  5. 如果连接方式过于复杂或不透明,无法有把握地生成,则生成测试骨架,并使用 TODO 标记需要连接组件的位置

跨模块测试本质上是集成测试。它们用于验证规范中的触发器链是否在组件边界之间得到忠实实现。单组件测试通过后,再优先处理这些测试。

复用现有测试

探索代码库时,请记录现有测试已经覆盖了哪些规范义务。一个覆盖了从提交事件到输出确认的成功路径的集成测试,已经覆盖了多个 rule_success 义务和端到端场景。

当现有测试覆盖某项规范义务时,请引用该测试,而不是生成重复测试。propagate 技能在集成层面的价值在于,根据规范义务列表验证覆盖是否完整、识别缺口,并生成测试来填补缺口——下方流程中的核对步骤会明确执行这一检查。用生成的等效测试替换有效的手写测试毫无价值。

对于延迟规范

延迟规范在单独的文件中有完整说明。当目标代码库不包含延迟规范对应的模块时,请生成一个带占位符的测试存根:

// TODO: deferred spec — InterviewerMatching.suggest
// This behaviour is specified as deferred. Provide a mock or skip.

流程

  1. 阅读规范 — 理解实体、规则、界面、不变量、转换图、依赖状态的字段、契约、配置和默认值。阅读评估规范,判断规范的成熟度。粗略的规范(包含实体和转换图,但没有规则)只能产生有限的测试义务——主要是结构测试。如果规范过于粗略,无法有意义地生成测试,请建议先使用 elicit 或 distill 技能进一步完善规范,再生成测试。包含规则和界面的规范支持完整的测试分类,包括数据流链测试和可达性测试。
  2. 阅读测试义务 — 来源为 allium plan 的输出或手动推导
  3. 阅读领域模型 — 来源为 allium model 的输出或手动推导
  4. 探索代码库 — 查找现有测试、测试框架、实体实现和规则实现
  5. 将构造映射到代码 — 将规范中的实体、规则和界面与实现类、函数或端点对应起来
  6. 生成测试 — 按照项目约定生成测试文件
  7. 验证测试能否编译和运行 — 确保生成的测试在语法上有效
  8. 核对义务 — 确认每项义务都对应一个覆盖它的测试,并自动补齐任何遗漏(参见义务核对)
探索清单

生成测试前,请先确认:

  • 测试框架和运行器(Jest、pytest、cargo test 等)
  • 是否存在 PBT 框架(fast-check、Hypothesis、proptest 等)
  • 测试文件的位置约定(与源文件同目录、__tests__/、tests/ 等)
  • 实体/模型的位置和模式(类、接口、结构体)
  • 测试数据的工厂/夹具模式
  • 状态转换的实现方式(方法、事件、状态机)
  • 界面的实现方式(路由、控制器、解析器)
  • 现有测试辅助工具或实用程序
  • 组件是否接受注入的时间源,以便进行时间相关测试
  • 是否存在可用于跨模块触发链的集成测试夹具
  • 现有测试已覆盖哪些规范义务
生成器注意事项

有生成器规范时,请用它们生成有效的测试数据:

  • 遵循字段类型和约束
  • 对于包含转换图的实体,生成处于特定生命周期状态的实体,并确保字段存在情况符合 when 子句(例如,shipped 状态的 Order 已填充 tracking_number 和 shipped_at;pending 状态的 Order 则没有)
  • 对于不变量,生成能测试边界条件的状态
  • 对于配置参数,除非在测试覆盖时进行覆盖,否则使用声明的默认值
义务核对

生成测试后,遍历完整的义务列表——来自 allium plan 输出或手动推导——并将每项义务对应到覆盖它的测试,无论该测试是在本次运行中生成还是预先存在。核对过程应保持静默:义务是内部工作数据,因此只要一切正常,就不要叙述对应关系、打印义务列表或提供覆盖报告。

对于没有测试覆盖的每项义务,尝试为其补充覆盖:

  1. 生成缺失的测试,或扩展已有测试以覆盖其尚未覆盖的部分
  2. 验证新测试能够编译并运行
  3. 根据结果重新核对该义务

这是一个小循环,需要使用与外层 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。
  • 运行时跟踪验证和模型检查属于独立的工作流。

评论

支持 Markdown