test(onboarding): 托管模板指纹测试,改内容未升版本即失败
All checks were successful
Push & PR Check / check (push) Successful in 1m3s

全托管文件(envoy.md / SKILL.md)按内容覆盖,但 verify_onboarding 只凭
capability-packs.json 版本判落后——只改内容忘升 BASE_ONBOARDING_PACK_VERSION
时 verify 永远 ready,子项目收不到更新。升版规则此前只有文档约束。

- 新增 managed_template_fingerprints_track_versions:「版本号 → 摘要」登记表
  TEMPLATE_FINGERPRINTS 钉住 resources/onboarding/**(↔ BASE 版本)与
  AGENTS / git-workflow 管理块(↔ 块首 version);摘要前 CRLF→LF 归一化
- capability-packs.json 与 BASE 相等复用既有断言,补可读报错
- onboarding-sync.md 说明该测试

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lanrtop 2026-10-03 01:34:33 +09:00
parent f948c4c802
commit 7595c37d22
4 changed files with 143 additions and 3 deletions

View File

@ -6,7 +6,7 @@ description: >
项目自身保留业务规则、测试、部署声明与交付决策。炼境本身亦被纳入其中,
形成可自我进化的开发智能闭环。
iteration: 1
updated: 2026-10-02
updated: 2026-10-03
agent_button_carrier: tauri
@ -376,7 +376,7 @@ modules:
progress: 100
note: "体系分发主线全卡完成:…/V1/U3/B4/U1 ✅(2026-07-15 择升环三断点修复收官:U3 规则校验 / B4 沉默项目信号 / U1 全托管升级+批量重分发)。U1 批量模式 MCP 实调待重建安装后终验,通过后模块收官 done"
position: [3500, 0]
updated: 2026-07-15
updated: 2026-10-03
- id: gitea-integration
name: Gitea 集成

View File

@ -168,6 +168,14 @@
- acceptance: agent_verify: [文件] AGENTS.md.tmpl「agent 按钮与验证路径」节含「同源同边界」「操作序列 agent 皆可复刻」「三分定位」表述,MANAGED 版本 1.6.1→1.7.0;炼境自身 AGENTS.md 块内同步;cargo test onboarding 通过
- notes: 2026-08-23 多角度辩证收敛(v3→v3.1 外科手术版):真实增量仅三个半(先本体后投影时序/复刻+复现路径/三分定位启发式/飞轮转译轻写一句),其余为已有六条重述不重复注入;三分定位从"定律"降级"启发式"(crypto.randomUUID 环境层实锤:本体投影都没病,病在环境);原型探索豁免时序约束;飞轮转译不承诺机制(采集通道挂 governance-feedback-loop 缺口①)
### ✅ F1. 托管模板指纹测试:改内容必须升版本(文档约束 → 机器校验)
- status: done
- done_note: 2026-10-03 完成。tests 新增 managed_template_fingerprints_track_versions + 「版本号 → 摘要」登记表 TEMPLATE_FINGERPRINTS 三项:resources/onboarding/** 全部资源(递归遍历,新增模板文件自动纳入)↔ BASE_ONBOARDING_PACK_VERSION;AGENTS / git-workflow 管理块 ↔ 块首 version。摘要前 CRLF→LF,段间 \0 分隔;Python 直接对 git index LF blob 独立复算三项摘要与 Windows CRLF 工作区结果一致。报错区分「内容变了版本没升」与「版本已升待更新指纹」,并给出可直接粘贴的新登记行。BASE 与模板 capability-packs.json 相等复用既有 check_capability_packs 断言(补可读报错)。自验:cargo test --locked onboarding 22 全过;envoy.md 改一字 → 仅 pack 项失败(其余 21 测仍全过,即旧测试抓不到该漏洞);AGENTS 块内改一字 → pack + AGENTS 两项失败;均已还原(envoy.md sha256 前后一致)后全绿。侦察佐证:dbc1b06(2026-09-23)改了 git-workflow 管理块内容未升 1.7.0,文档规则在 07-06 之后仍被漏过。**合入前实证更新流程**:rebase 到并行合入的 PR #28(envoy/SKILL/AGENTS 模板改动,规范升 BASE 1.7.0、AGENTS 块 1.10.0)后测试按设计报「版本已升,更新指纹常量即可」并给出新登记行(git-workflow 未动不报),照报错更新登记表后 22 全过、全量 cargo test 267 过
- complexity: S
- files: src-tauri/src/commands/onboarding.rs, .claude/rules/onboarding-sync.md
- acceptance: agent_verify: [cargo test] `cd src-tauri && cargo test --locked onboarding` 全绿;临时改 resources/onboarding/.claude/commands/envoy.md 一个字 → 指纹测试失败且报错含「升 BASE_ONBOARDING_PACK_VERSION 与 capability-packs.json 版本,并更新指纹常量」,还原后通过;摘要前 CRLF→LF 归一化(工作区 autocrlf=CRLF 与 CI LF 同摘要)
- notes: 全托管文件(envoy.md / SKILL.md)按内容覆盖,但 verify 只看 capability-packs 版本(check_skill 仅子串检查)——只改内容忘升 BASE_ONBOARDING_PACK_VERSION 时 verify 永远 ready、无人重跑分发,子项目收不到更新。onboarding-sync.md 的升版规则此前只有文档约束(2026-07-06 块版本漏升实锤)。指纹表同时钉住 AGENTS / git-workflow 的 ONBOARDING_MANAGED 块版本(同类漏洞:块版本不升 → dispatch_versioned 全部 skipped)
## 复盘笔记
【复盘】U1 全托管文件升级通道 + 批量重分发(2026-07-15)

View File

@ -5,4 +5,6 @@ globs: src-tauri/resources/onboarding/**
**改模板内容必须同步升级 `ONBOARDING_MANAGED_START version="x.y.z"` 版本号**——版本是分发引擎唯一的升级信号,内容变了版本不动,批量分发会全部 skipped(2026-07-06 实测踩坑)。模板改完后跑 `cargo test onboarding` 确认版本相关测试通过。
该规则已由指纹测试 `managed_template_fingerprints_track_versions`(`onboarding.rs`)机器校验:`resources/onboarding/**` 任何内容变化须升 `BASE_ONBOARDING_PACK_VERSION` 与模板 `capability-packs.json` 版本,管理块内容变化另须升块首 version,再按测试报错把新摘要填回 `TEMPLATE_FINGERPRINTS`。
分发到子项目需要**重建安装炼境**(模板打包在二进制资源里),再调 `apply_onboarding_pack`。

View File

@ -1263,7 +1263,12 @@ mod tests {
assert_eq!(unavailable.state, VerifyState::Unavailable);
let pack_ready = check_capability_packs(Some(TMPL_CAPABILITY_PACKS));
assert_eq!(pack_ready.state, VerifyState::Ready);
assert_eq!(
pack_ready.state,
VerifyState::Ready,
"模板 capability-packs.json 的版本须等于 BASE_ONBOARDING_PACK_VERSION / TOOLCHAIN_PACK_VERSION:{}",
pack_ready.detail
);
let pack_old = check_capability_packs(Some(r#"{"schema_version":0,"packs":[]}"#));
assert_eq!(pack_old.state, VerifyState::VersionMismatch);
@ -1442,6 +1447,131 @@ mod tests {
);
}
// ── 托管模板指纹:改内容必须升版本(onboarding-sync.md 升版规则的机器校验)──
/// 版本号 → 摘要登记表:(指纹对象, 登记版本, sha256)。
/// - `resources/onboarding/**`:全部接入包资源,对应 BASE_ONBOARDING_PACK_VERSION——
/// verify 只凭 capability-packs.json 的版本判落后,全托管文件(envoy/SKILL)的内容不参与比对
/// - `<模板>#ONBOARDING_MANAGED`:版本化块,对应块首 version——dispatch_versioned 唯一的升级信号
///
/// 改了模板:按测试报错升对应版本,再把报错给出的新摘要填回这里。
const TEMPLATE_FINGERPRINTS: [(&str, &str, &str); 3] = [
(
"resources/onboarding/**",
"1.7.0",
"0c39acd7c0513a1063ea0c2de5da06ba2baa69b7eb85b167499859ab6ad0eb45",
),
(
"AGENTS.md.tmpl#ONBOARDING_MANAGED",
"1.10.0",
"59b29e99c1b2184732dbf828eb3f2c8d3bb5afdfdbd30fd5d967daced82313e5",
),
(
"git-workflow.md.tmpl#ONBOARDING_MANAGED",
"1.7.0",
"9d52231fd725b786ad4d2bf874e21c155150b4df5a115e3b8facf23b6c23c255",
),
];
/// sha256 hex。先把 CRLF 归一化为 LF:Windows autocrlf 工作区是 CRLF,CI 检出是 LF。
/// 段间加 \0 分隔,内容在相邻段之间挪动也会改变摘要。
fn lf_sha256<'a>(parts: impl IntoIterator<Item = &'a str>) -> String {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
for part in parts {
hasher.update(part.replace("\r\n", "\n").as_bytes());
hasher.update([0u8]);
}
hasher
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
/// 递归收集 (相对路径, 内容)——新增模板文件无需登记,自动纳入指纹。
fn collect_resource_files(root: &Path, dir: &Path, out: &mut Vec<(String, String)>) {
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
collect_resource_files(root, &path, out);
continue;
}
let relative = path
.strip_prefix(root)
.unwrap()
.to_string_lossy()
.replace('\\', "/");
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("读取模板 {relative} 失败: {e}"));
out.push((relative, content));
}
}
#[test]
fn managed_template_fingerprints_track_versions() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/onboarding");
let mut files = Vec::new();
collect_resource_files(&root, &root, &mut files);
files.sort();
let pack_digest = lf_sha256(
files
.iter()
.flat_map(|(path, content)| [path.as_str(), content.as_str()]),
);
let block = |template: &'static str| {
let block = extract_managed_block(template).expect("模板须含 ONBOARDING_MANAGED 块");
(
block_version(block).expect("管理块首须有 version"),
lf_sha256([block]),
)
};
let (agents_version, agents_digest) = block(TMPL_AGENTS);
let (workflow_version, workflow_digest) = block(TMPL_GIT_WORKFLOW);
let actual = [
(
BASE_ONBOARDING_PACK_VERSION,
pack_digest,
"改了托管模板请升 BASE_ONBOARDING_PACK_VERSION 与 capability-packs.json 版本,并更新指纹常量",
),
(
agents_version,
agents_digest,
"改了 AGENTS.md.tmpl 管理块请升块首 version(不升则存量项目重跑接入包全部 skipped),并更新指纹常量",
),
(
workflow_version,
workflow_digest,
"改了 git-workflow.md.tmpl 管理块请升块首 version(不升则存量项目重跑接入包全部 skipped),并更新指纹常量",
),
];
let stale: Vec<String> = TEMPLATE_FINGERPRINTS
.iter()
.zip(&actual)
.filter(|((_, version, digest), (now_version, now_digest, _))| {
*version != *now_version || *digest != *now_digest
})
.map(|((name, version, _), (now_version, now_digest, hint))| {
let (advice, next_version) = if version == now_version {
(*hint, "<新版本>")
} else {
("版本已升,更新指纹常量即可", *now_version)
};
format!(
"- {name}:登记 v{version},当前 v{now_version}——{advice}。\n TEMPLATE_FINGERPRINTS 改为 (\"{name}\", \"{next_version}\", \"{now_digest}\")"
)
})
.collect();
let hashed: Vec<&str> = files.iter().map(|(path, _)| path.as_str()).collect();
assert!(
stale.is_empty(),
"托管模板内容与登记版本不符:\n{}\n(resources/onboarding/** 参与摘要的文件:{})",
stale.join("\n"),
hashed.join(", ")
);
}
#[test]
fn skill_is_projected_to_codex_and_claude_from_one_source() {
let base = std::env::temp_dir().join(format!(