@ -17,8 +17,15 @@ const TMPL_AGENTS: &str = include_str!("../../resources/onboarding/AGENTS.md.tmp
const TMPL_LEFTHOOK : & str = include_str! ( " ../../resources/onboarding/lefthook.yml.tmpl " ) ;
const TMPL_GIT_WORKFLOW : & str = include_str! ( " ../../resources/onboarding/git-workflow.md.tmpl " ) ;
const TMPL_ENVOY : & str = include_str! ( " ../../resources/onboarding/.claude/commands/envoy.md " ) ;
const TMPL_BLUEPRINT_GATE : & str = include_str! ( " ../../resources/onboarding/.claude/hooks/blueprint-gate.cjs " ) ;
const TMPL_LIANJING_SKILL : & str = include_str! ( " ../../resources/onboarding/.agents/skills/lian-jing/SKILL.md " ) ;
const TMPL_BLUEPRINT_GATE : & str =
include_str! ( " ../../resources/onboarding/.claude/hooks/blueprint-gate.cjs " ) ;
const TMPL_LIANJING_SKILL : & str =
include_str! ( " ../../resources/onboarding/.agents/skills/lian-jing/SKILL.md " ) ;
const TMPL_CAPABILITY_PACKS : & str =
include_str! ( " ../../resources/onboarding/.lian-jing/capability-packs.json " ) ;
const PROJECT_CONTEXT_SCHEMA_VERSION : u64 = 1 ;
const CAPABILITY_PACK_SCHEMA_VERSION : u64 = 1 ;
const BASE_ONBOARDING_PACK_VERSION : & str = " 1.0.0 " ;
/// 单个模板的分发结果。
#[ derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq) ]
@ -134,29 +141,32 @@ fn dispatch_versioned(root: &Path, relative: &str, template: &str) -> TemplateRe
}
} ;
let ( new_content , action , detail ) = if let ( Some ( s ) , Some ( e ) ) =
( content . find ( MANAGED_START ) , content . find ( MANAGED_END ) )
{
let old_ver = block_version ( & content ) . unwrap_or ( " ? " ) . to_string ( ) ;
if old_ver = = tpl_ver {
return TemplateResult {
name : relative . to_string ( ) ,
action : " skipped " . into ( ) ,
detail : format ! ( " 管理块已是 v{tpl_ver} " ) ,
} ;
}
let mut nc = String ::new ( ) ;
nc . push_str ( & content [ .. s ] ) ;
nc . push_str ( tpl_block ) ;
nc . push_str ( & content [ e + MANAGED_END . len ( ) .. ] ) ;
( nc , " updated " , format! ( " 管理块 v {old_ver} → v {tpl_ver} ,块外内容未动 " ) )
} else {
(
format! ( " {} \n \n {} \n " , content . trim_end ( ) , tpl_block ) ,
" appended " ,
format! ( " 追加管理块 v {tpl_ver} (存量文件无标记,原内容保留在上方) " ) ,
)
} ;
let ( new_content , action , detail ) =
if let ( Some ( s ) , Some ( e ) ) = ( content . find ( MANAGED_START ) , content . find ( MANAGED_END ) ) {
let old_ver = block_version ( & content ) . unwrap_or ( " ? " ) . to_string ( ) ;
if old_ver = = tpl_ver {
return TemplateResult {
name : relative . to_string ( ) ,
action : " skipped " . into ( ) ,
detail : format ! ( " 管理块已是 v{tpl_ver} " ) ,
} ;
}
let mut nc = String ::new ( ) ;
nc . push_str ( & content [ .. s ] ) ;
nc . push_str ( tpl_block ) ;
nc . push_str ( & content [ e + MANAGED_END . len ( ) .. ] ) ;
(
nc ,
" updated " ,
format! ( " 管理块 v {old_ver} → v {tpl_ver} ,块外内容未动 " ) ,
)
} else {
(
format! ( " {} \n \n {} \n " , content . trim_end ( ) , tpl_block ) ,
" appended " ,
format! ( " 追加管理块 v {tpl_ver} (存量文件无标记,原内容保留在上方) " ) ,
)
} ;
match std ::fs ::write ( & target , new_content ) {
Ok ( _ ) = > TemplateResult {
@ -181,8 +191,21 @@ pub fn dispatch_templates(project_path: &str) -> Vec<TemplateResult> {
dispatch_one ( root , " lefthook.yml " , TMPL_LEFTHOOK ) ,
dispatch_versioned ( root , " docs/ai-context/git-workflow.md " , TMPL_GIT_WORKFLOW ) ,
dispatch_fully_managed ( root , " .claude/commands/envoy.md " , TMPL_ENVOY ) ,
dispatch_fully_managed ( root , " .claude/hooks/blueprint-gate.cjs " , TMPL_BLUEPRINT_GATE ) ,
dispatch_fully_managed ( root , " .agents/skills/lian-jing/SKILL.md " , TMPL_LIANJING_SKILL ) ,
dispatch_fully_managed (
root ,
" .claude/hooks/blueprint-gate.cjs " ,
TMPL_BLUEPRINT_GATE ,
) ,
dispatch_fully_managed (
root ,
" .agents/skills/lian-jing/SKILL.md " ,
TMPL_LIANJING_SKILL ,
) ,
dispatch_fully_managed (
root ,
" .lian-jing/capability-packs.json " ,
TMPL_CAPABILITY_PACKS ,
) ,
]
}
@ -220,7 +243,11 @@ fn run_lefthook_install(root: &Path) -> HookActivation {
}
for ( program , source ) in & attempts {
match crate ::silent_cmd ( program ) . arg ( " install " ) . current_dir ( root ) . output ( ) {
match crate ::silent_cmd ( program )
. arg ( " install " )
. current_dir ( root )
. output ( )
{
Ok ( out ) if out . status . success ( ) = > {
return HookActivation {
status : " activated " . into ( ) ,
@ -275,7 +302,11 @@ pub fn apply_onboarding_pack(
let templates = dispatch_templates ( & project_path ) ;
let hook = run_lefthook_install ( Path ::new ( project_path . trim_end_matches ( [ '/' , '\\' ] ) ) ) ;
let buff = crate ::commands ::buff ::apply_blueprint_buff ( project_path , project_id ) ? ;
Ok ( OnboardingReport { templates , hook , buff } )
Ok ( OnboardingReport {
templates ,
hook ,
buff ,
} )
}
// ── V1: 接入生效验收( verify_onboarding 检查链,判定逻辑纯函数化可单测)──────
@ -285,9 +316,170 @@ pub fn apply_onboarding_pack(
pub struct VerifyCheck {
pub name : String ,
pub pass : bool ,
pub state : VerifyState ,
pub detail : String ,
}
/// 接入检查的可行动状态。只有 ready 才表示检查项通过;其余状态必须显式说明原因。
#[ derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq) ]
#[ serde(rename_all = " snake_case " ) ]
pub enum VerifyState {
Ready ,
Missing ,
Legacy ,
VersionMismatch ,
Unavailable ,
}
impl VerifyCheck {
fn with_state ( name : impl Into < String > , state : VerifyState , detail : impl Into < String > ) -> Self {
let pass = state = = VerifyState ::Ready ;
Self {
name : name . into ( ) ,
pass ,
state ,
detail : detail . into ( ) ,
}
}
fn from_pass ( name : impl Into < String > , pass : bool , detail : impl Into < String > ) -> Self {
Self ::with_state (
name ,
if pass {
VerifyState ::Ready
} else {
VerifyState ::Missing
} ,
detail ,
)
}
}
fn check_project_context (
canonical : Option < & str > ,
legacy : Option < & str > ,
expected_project_id : & str ,
) -> VerifyCheck {
let parse_object = | content : & str | serde_json ::from_str ::< serde_json ::Value > ( content ) . ok ( ) ;
if canonical . is_some ( ) & & canonical . and_then ( parse_object ) . is_none ( ) {
return VerifyCheck ::with_state (
" 项目身份上下文 " ,
VerifyState ::Unavailable ,
" .lian-jing/context.json 不是有效 JSON——请修复后重试 inject_mcp " ,
) ;
}
match canonical . and_then ( parse_object ) {
Some ( context ) if ! context . is_object ( ) = > VerifyCheck ::with_state (
" 项目身份上下文 " ,
VerifyState ::Unavailable ,
" .lian-jing/context.json 顶层不是 JSON 对象——请修复后重试 inject_mcp " ,
) ,
Some ( context ) = > {
let version = context
. get ( " schema_version " )
. and_then ( | value | value . as_u64 ( ) ) ;
let project_id = context . get ( " project_id " ) . and_then ( | value | value . as_str ( ) ) ;
if version ! = Some ( PROJECT_CONTEXT_SCHEMA_VERSION ) {
VerifyCheck ::with_state (
" 项目身份上下文 " ,
VerifyState ::VersionMismatch ,
format! (
" context schema_version={:?},当前为 {}——调用 inject_mcp 可升级 " ,
version , PROJECT_CONTEXT_SCHEMA_VERSION
) ,
)
} else if project_id = = Some ( expected_project_id ) {
VerifyCheck ::with_state (
" 项目身份上下文 " ,
VerifyState ::Ready ,
" `.lian-jing/context.json` 与已登记 project_id 一致 " ,
)
} else {
VerifyCheck ::with_state (
" 项目身份上下文 " ,
VerifyState ::Unavailable ,
" `.lian-jing/context.json` 的 project_id 与炼境登记不一致——停止使用并重新 inject_mcp " ,
)
}
}
None if legacy
. and_then ( parse_object )
. and_then ( | value | {
value
. get ( " project_id " )
. and_then ( | value | value . as_str ( ) )
. map ( str ::to_owned )
} )
. as_deref ( )
= = Some ( expected_project_id ) = >
{
VerifyCheck ::with_state (
" 项目身份上下文 " ,
VerifyState ::Legacy ,
" 仅发现 `.claude/liangjing.json`——调用 inject_mcp 迁移到 `.lian-jing/context.json` " ,
)
}
None if legacy . is_some ( ) = > VerifyCheck ::with_state (
" 项目身份上下文 " ,
VerifyState ::Unavailable ,
" `.claude/liangjing.json` 无有效 project_id——请重新 inject_mcp " ,
) ,
None = > VerifyCheck ::with_state (
" 项目身份上下文 " ,
VerifyState ::Missing ,
" 缺少 `.lian-jing/context.json`——调用 inject_mcp 创建项目身份上下文 " ,
) ,
}
}
fn check_capability_packs ( content : Option < & str > ) -> VerifyCheck {
let Some ( content ) = content else {
return VerifyCheck ::with_state (
" 基础能力包描述符 " ,
VerifyState ::Missing ,
" 缺少 `.lian-jing/capability-packs.json`——重跑接入包可写入 " ,
) ;
} ;
let Ok ( descriptor ) = serde_json ::from_str ::< serde_json ::Value > ( content ) else {
return VerifyCheck ::with_state (
" 基础能力包描述符 " ,
VerifyState ::Unavailable ,
" 能力包描述符不是有效 JSON——请修复后重跑接入包 " ,
) ;
} ;
let schema_version = descriptor
. get ( " schema_version " )
. and_then ( | value | value . as_u64 ( ) ) ;
let version = descriptor
. get ( " packs " )
. and_then ( | value | value . as_array ( ) )
. and_then ( | packs | {
packs . iter ( ) . find ( | pack | {
pack . get ( " pack_id " ) . and_then ( | value | value . as_str ( ) ) = = Some ( " base-onboarding " )
} )
} )
. and_then ( | pack | pack . get ( " version " ) )
. and_then ( | value | value . as_str ( ) ) ;
if schema_version = = Some ( CAPABILITY_PACK_SCHEMA_VERSION )
& & version = = Some ( BASE_ONBOARDING_PACK_VERSION )
{
VerifyCheck ::with_state (
" 基础能力包描述符 " ,
VerifyState ::Ready ,
format! ( " base-onboarding v {BASE_ONBOARDING_PACK_VERSION} (最新) " ) ,
)
} else {
VerifyCheck ::with_state (
" 基础能力包描述符 " ,
VerifyState ::VersionMismatch ,
format! (
" 能力包 schema={schema_version:?}、base-onboarding={version:?};当前应为 schema={}、v{}——重跑接入包可升级 " ,
CAPABILITY_PACK_SCHEMA_VERSION , BASE_ONBOARDING_PACK_VERSION
) ,
)
}
}
/// 纯函数: pre-commit hook 内容是否由 lefthook 接管。
fn hook_is_lefthook ( content : & str ) -> bool {
content . contains ( " lefthook " )
@ -299,7 +491,9 @@ fn parse_origin_url(git_config: &str) -> Option<String> {
for line in git_config . lines ( ) {
let t = line . trim ( ) ;
if t . starts_with ( '[' ) {
in_origin = t . replace ( ' ' , " " ) . eq_ignore_ascii_case ( " [remote \" origin \" ] " ) ;
in_origin = t
. replace ( ' ' , " " )
. eq_ignore_ascii_case ( " [remote \" origin \" ] " ) ;
continue ;
}
if in_origin {
@ -340,15 +534,24 @@ fn check_codex_mcp(content: Option<&str>) -> (bool, String) {
{
( true , " lian-jing 指向本地 MCP 服务 " . into ( ) )
}
Some ( _ ) = > ( false , " 未找到炼境的 Codex MCP 配置——调用 inject_mcp 修复 " . into ( ) ) ,
None = > ( false , " .codex/config.toml 缺失——调用 inject_mcp 后生成 " . into ( ) ) ,
Some ( _ ) = > (
false ,
" 未找到炼境的 Codex MCP 配置——调用 inject_mcp 修复 " . into ( ) ,
) ,
None = > (
false ,
" .codex/config.toml 缺失——调用 inject_mcp 后生成 " . into ( ) ,
) ,
}
}
fn check_skill ( content : Option < & str > ) -> ( bool , String ) {
match content {
Some ( content ) if content . contains ( " name: lian-jing " ) = > ( true , " 炼境项目技能已分发 " . into ( ) ) ,
_ = > ( false , " 缺少 .agents/skills/lian-jing/SKILL.md——重跑接入包 " . into ( ) ) ,
_ = > (
false ,
" 缺少 .agents/skills/lian-jing/SKILL.md——重跑接入包 " . into ( ) ,
) ,
}
}
@ -356,7 +559,10 @@ fn check_skill(content: Option<&str>) -> (bool, String) {
fn check_managed_version ( content : Option < & str > , expected : & str ) -> ( bool , String ) {
match content . and_then ( block_version ) {
Some ( v ) if v = = expected = > ( true , format! ( " 管理块 v {v} (最新) " ) ) ,
Some ( v ) = > ( false , format! ( " 管理块 v {v} ,最新模板 v {expected} ,重跑接入包可升级 " ) ) ,
Some ( v ) = > (
false ,
format! ( " 管理块 v {v} ,最新模板 v {expected} ,重跑接入包可升级 " ) ,
) ,
None = > ( false , " 文件缺失或无管理块标记 " . into ( ) ) ,
}
}
@ -373,7 +579,10 @@ fn check_origin(origin_host: Option<&str>, instance_hosts: &[String]) -> (bool,
}
Some ( h ) = > (
false ,
format! ( " origin= {h} ,不在已登记 Gitea 实例( {} )中 " , instance_hosts . join ( " , " ) ) ,
format! (
" origin={h},不在已登记 Gitea 实例({})中 " ,
instance_hosts . join ( " , " )
) ,
) ,
}
}
@ -430,7 +639,7 @@ fn check_lefthook_keys(project_yml: Option<&str>, template_yml: &str) -> (bool,
}
}
/// 接入生效验收: 七项 检查链。分发成功 ≠ 功能生效,本函数验证"真的用起来了" 。
/// 接入生效验收: 检查链覆盖项目身份、能力包和既有接入基线 。分发成功 ≠ 功能生效。
/// conn 由调用方注入( R06) 。
pub fn verify_onboarding (
conn : & rusqlite ::Connection ,
@ -449,27 +658,44 @@ pub fn verify_onboarding(
" git-workflow 管理块版本(含交付状态调用说明) " ,
) ,
] {
let expected = extract_managed_block ( tmpl ) . and_then ( block_version ) . unwrap_or ( " ? " ) ;
let expected = extract_managed_block ( tmpl )
. and_then ( block_version )
. unwrap_or ( " ? " ) ;
let ( pass , detail ) = check_managed_version ( read ( rel ) . as_deref ( ) , expected ) ;
checks . push ( VerifyCheck { name : label . into ( ) , pass , detail } ) ;
checks . push ( VerifyCheck ::from_pass ( label , pass , detail ) ) ;
}
checks . push ( check_project_context (
read ( " .lian-jing/context.json " ) . as_deref ( ) ,
read ( " .claude/liangjing.json " ) . as_deref ( ) ,
project_id ,
) ) ;
checks . push ( check_capability_packs (
read ( " .lian-jing/capability-packs.json " ) . as_deref ( ) ,
) ) ;
let ( pass , detail ) = check_skill ( read ( " .agents/skills/lian-jing/SKILL.md " ) . as_deref ( ) ) ;
checks . push ( VerifyCheck { name : " Codex 炼境技能已分发 " . into ( ) , pass , detail } ) ;
checks . push ( VerifyCheck ::from_pass ( " Codex 炼境技能已分发 " , pass , detail ) ) ;
let ( pass , detail ) = check_codex_mcp ( read ( " .codex/config.toml " ) . as_deref ( ) ) ;
checks . push ( VerifyCheck { name : " Codex MCP 已配置 " . into ( ) , pass , detail } ) ;
checks . push ( VerifyCheck ::from_pass ( " Codex MCP 已配置 " , pass , detail ) ) ;
// 3 lefthook hook 真挂在 .git/hooks
let ( pass , detail ) = match read ( " .git/hooks/pre-commit " ) {
Some ( c ) if hook_is_lefthook ( & c ) = > ( true , " pre-commit 由 lefthook 接管 " . to_string ( ) ) ,
Some ( _ ) = > ( false , " pre-commit 存在但非 lefthook( 被其他工具占用? ) " . to_string ( ) ) ,
None = > ( false , " 无 pre-commit hook——lefthook install 未执行 " . to_string ( ) ) ,
Some ( _ ) = > (
false ,
" pre-commit 存在但非 lefthook( 被其他工具占用? ) " . to_string ( ) ,
) ,
None = > (
false ,
" 无 pre-commit hook——lefthook install 未执行 " . to_string ( ) ,
) ,
} ;
checks . push ( VerifyCheck { name : " lefthook hook 挂载 " . into ( ) , pass , detail } ) ;
checks . push ( VerifyCheck ::from_pass ( " lefthook hook 挂载 " , pass , detail ) ) ;
// 4 lefthook 检查段齐全( 期望键从内嵌模板自省, U1)
let ( pass , detail ) = check_lefthook_keys ( read ( " lefthook.yml " ) . as_deref ( ) , TMPL_LEFTHOOK ) ;
checks . push ( VerifyCheck { name : " lefthook 检查段齐全 " . into ( ) , pass , detail } ) ;
checks . push ( VerifyCheck ::from_pass ( " lefthook 检查段齐全 " , pass , detail ) ) ;
// 5 origin 指向已登记 Gitea 实例
let origin_host = read ( " .git/config " )
@ -488,7 +714,7 @@ pub fn verify_onboarding(
}
}
let ( pass , detail ) = check_origin ( origin_host . as_deref ( ) , & instance_hosts ) ;
checks . push ( VerifyCheck { name : " origin 指向 Gitea " .into ( ) , pass , detail } ) ;
checks . push ( VerifyCheck ::from_pass ( " origin 指向 Gitea " , pass , detail ) ) ;
// 6 CI workflow 存在
let yml_count = std ::fs ::read_dir ( root . join ( " .gitea/workflows " ) )
@ -503,15 +729,15 @@ pub fn verify_onboarding(
. count ( )
} )
. unwrap_or ( 0 ) ;
checks . push ( VerifyCheck {
name : " CI workflow 存在 " . into ( ) ,
pass: yml_count > 0 ,
detail : if yml_count > 0 {
checks . push ( VerifyCheck ::from_pass (
" CI workflow 存在 " ,
yml_count > 0 ,
if yml_count > 0 {
format! ( " .gitea/workflows/ 有 {yml_count} 个 workflow " )
} else {
" 无 .gitea/workflows/*.yml——可调 distribute_cicd 分发 " . into ( )
} ,
} ) ;
) ) ;
// 7 飞轮数据在流入(近 30 天 commit_metrics)
let recent : i64 = conn
@ -521,15 +747,15 @@ pub fn verify_onboarding(
| r | r . get ( 0 ) ,
)
. unwrap_or ( 0 ) ;
checks . push ( VerifyCheck {
name : " 飞轮 commit 数据流入 " . into ( ) ,
pass: recent > 0 ,
detail : if recent > 0 {
checks . push ( VerifyCheck ::from_pass (
" 飞轮 commit 数据流入 " ,
recent > 0 ,
if recent > 0 {
format! ( " 近 30 天 {recent} 条 commit 已采集 " )
} else {
" 近 30 天无数据——确认 buff 已挂且有 commit 活动(或调 poll_now) " . into ( )
} ,
} ) ;
) ) ;
checks
}
@ -577,8 +803,8 @@ mod tests {
#[ test ]
fn writes_then_skips_never_overwrites ( ) {
let base = std ::env ::temp_dir ( )
. join ( format! ( " lianjing_onboarding_test_ {} " , std ::process ::id ( ) ) ) ;
let base =
std ::env ::temp_dir ( ) . join ( format! ( " lianjing_onboarding_test_ {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & base ) ;
std ::fs ::create_dir_all ( & base ) . unwrap ( ) ;
let pp = base . to_str ( ) . unwrap ( ) ;
@ -608,8 +834,8 @@ mod tests {
#[ test ]
fn versioned_dispatch_three_states ( ) {
let base = std ::env ::temp_dir ( )
. join ( format! ( " lianjing_onb_ver_test_ {} " , std ::process ::id ( ) ) ) ;
let base =
std ::env ::temp_dir ( ) . join ( format! ( " lianjing_onb_ver_test_ {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & base ) ;
std ::fs ::create_dir_all ( & base ) . unwrap ( ) ;
let pp = base . to_str ( ) . unwrap ( ) ;
@ -631,13 +857,13 @@ mod tests {
. and_then ( block_version )
. expect ( " 模板必须带版本标记 " ) ;
let old = agents
. replace ( & format! ( " version= \" {tpl_ver} \" " ) , " version= \" 1.0.0 \" " )
. replace (
& format! ( " version= \" {tpl_ver} \" " ) ,
" version= \" 1.0.0 \" " ,
)
. replace ( " ## 项目概况 " , " ## 项目概况
" ## 项目概况 " ,
" ## 项目概况
我 的 项 目 说 明 " )
我 的 项 目 说 明 " ,
)
+ "
## 用 户 自 加 的 章 节
内 容
@ -651,14 +877,24 @@ mod tests {
upgraded . contains ( & format! ( " version= \" {tpl_ver} \" " ) ) ,
" 块应升级到模板版本 "
) ;
assert! ( upgraded . contains ( " 我的项目说明 " ) , " 块外用户内容(前)须保留 " ) ;
assert! ( upgraded . contains ( " 用户自加的章节 " ) , " 块外用户内容(后)须保留 " ) ;
assert! (
upgraded . contains ( " 我的项目说明 " ) ,
" 块外用户内容(前)须保留 "
) ;
assert! (
upgraded . contains ( " 用户自加的章节 " ) ,
" 块外用户内容(后)须保留 "
) ;
assert_eq! ( upgraded . matches ( MANAGED_START ) . count ( ) , 1 , " 只有一个管理块 " ) ;
// 态三:存量文件无标记 → appended, 原内容保留在上方
std ::fs ::write ( base . join ( " AGENTS.md " ) , " # 手写的 AGENTS
std ::fs ::write (
base . join ( " AGENTS.md " ) ,
" # 手写的 AGENTS
自 定 义 内 容 " ).unwrap();
自 定 义 内 容 " ,
)
. unwrap ( ) ;
let r4 = dispatch_templates ( pp ) ;
let agents_r = r4 . iter ( ) . find ( | t | t . name = = " AGENTS.md " ) . unwrap ( ) ;
assert_eq! ( agents_r . action , " appended " ) ;
@ -677,8 +913,14 @@ mod tests {
#[ test ]
fn hook_detection_covers_mounted_and_foreign ( ) {
assert! ( hook_is_lefthook ( " #!/bin/sh \n call lefthook run pre-commit " ) , " lefthook 接管应判 true " ) ;
assert! ( ! hook_is_lefthook ( " #!/bin/sh \n npx husky run " ) , " 非 lefthook hook 应判 false " ) ;
assert! (
hook_is_lefthook ( " #!/bin/sh \n call lefthook run pre-commit " ) ,
" lefthook 接管应判 true "
) ;
assert! (
! hook_is_lefthook ( " #!/bin/sh \n npx husky run " ) ,
" 非 lefthook hook 应判 false "
) ;
}
#[ test ]
@ -689,41 +931,109 @@ mod tests {
Some ( " https://git.example.com/o/r.git " ) ,
" 应取 origin 而非其他 remote "
) ;
assert_eq! ( parse_origin_url ( " [core] \n \t bare = false " ) , None , " 无 origin 应返回 None " ) ;
assert_eq! (
parse_origin_url ( " [core] \n \t bare = false " ) ,
None ,
" 无 origin 应返回 None "
) ;
assert_eq! ( url_host ( " https://git.example.com/o/r.git " ) . as_deref ( ) , Some ( " git.example.com " ) ) ;
assert_eq! ( url_host ( " https://user@git.example.com:3000/o/r.git " ) . as_deref ( ) , Some ( " git.example.com " ) , " 应忽略用户名与端口 " ) ;
assert_eq! ( url_host ( " git@git.example.com:o/r.git " ) . as_deref ( ) , Some ( " git.example.com " ) , " ssh 形式 " ) ;
assert_eq! (
url_host ( " https://git.example.com/o/r.git " ) . as_deref ( ) ,
Some ( " git.example.com " )
) ;
assert_eq! (
url_host ( " https://user@git.example.com:3000/o/r.git " ) . as_deref ( ) ,
Some ( " git.example.com " ) ,
" 应忽略用户名与端口 "
) ;
assert_eq! (
url_host ( " git@git.example.com:o/r.git " ) . as_deref ( ) ,
Some ( " git.example.com " ) ,
" ssh 形式 "
) ;
}
#[ test ]
fn origin_check_match_and_mismatch ( ) {
let hosts = vec! [ " git.example.com " . to_string ( ) ] ;
assert! ( check_origin ( Some ( " git.example.com " ) , & hosts ) . 0 , " 命中实例应 pass " ) ;
assert! (
check_origin ( Some ( " git.example.com " ) , & hosts ) . 0 ,
" 命中实例应 pass "
) ;
assert! ( ! check_origin ( Some ( " github.com " ) , & hosts ) . 0 , " 未命中应 fail " ) ;
assert! ( ! check_origin ( None , & hosts ) . 0 , " 无 origin 应 fail " ) ;
assert! ( ! check_origin ( Some ( " git.example.com " ) , & [ ] ) . 0 , " 无登记实例应 fail " ) ;
assert! (
! check_origin ( Some ( " git.example.com " ) , & [ ] ) . 0 ,
" 无登记实例应 fail "
) ;
}
#[ test ]
fn project_context_and_pack_checks_report_actionable_states ( ) {
let ready = check_project_context (
Some ( r # "{"schema_version":1,"project_id":"p1"}"# ) ,
None ,
" p1 " ,
) ;
assert_eq! ( ready . state , VerifyState ::Ready ) ;
assert! ( ready . pass ) ;
let legacy = check_project_context ( None , Some ( r # "{"project_id":"p1"}"# ) , " p1 " ) ;
assert_eq! ( legacy . state , VerifyState ::Legacy ) ;
assert! ( ! legacy . pass ) ;
let mismatch = check_project_context (
Some ( r # "{"schema_version":0,"project_id":"p1"}"# ) ,
None ,
" p1 " ,
) ;
assert_eq! ( mismatch . state , VerifyState ::VersionMismatch ) ;
let missing = check_project_context ( None , None , " p1 " ) ;
assert_eq! ( missing . state , VerifyState ::Missing ) ;
let unavailable = check_project_context ( Some ( " not-json " ) , None , " p1 " ) ;
assert_eq! ( unavailable . state , VerifyState ::Unavailable ) ;
let pack_ready = check_capability_packs ( Some ( TMPL_CAPABILITY_PACKS ) ) ;
assert_eq! ( pack_ready . state , VerifyState ::Ready ) ;
let pack_old = check_capability_packs ( Some ( r # "{"schema_version":0,"packs":[]}"# ) ) ;
assert_eq! ( pack_old . state , VerifyState ::VersionMismatch ) ;
}
#[ test ]
fn verify_onboarding_full_chain ( ) {
let conn = crate ::db ::conn_with_schema ( ) ;
let base = std ::env ::temp_dir ( )
. join ( format! ( " lianjing_verify_test_ {} " , std ::process ::id ( ) ) ) ;
let base =
std ::env ::temp_dir ( ) . join ( format! ( " lianjing_verify_test_ {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & base ) ;
std ::fs ::create_dir_all ( & base ) . unwrap ( ) ;
// 空目录:七项全不通过(真实反映"什么都没接")
// 空目录: 所有检查均 不通过(真实反映"什么都没接")
let checks = verify_onboarding ( & conn , " p1 " , & base ) ;
assert_eq! ( checks . len ( ) , 9 , " 检查链应为九项 " ) ;
assert_eq! ( checks . len ( ) , 11, " 检查链应为十一 项" ) ;
assert! ( checks . iter ( ) . all ( | c | ! c . pass ) , " 空项目应全部不通过 " ) ;
// 铺齐全部生效条件后:七项全通过
// 铺齐全部生效条件后: 所有检查 全通过
dispatch_templates ( base . to_str ( ) . unwrap ( ) ) ;
std ::fs ::create_dir_all ( base . join ( " .lian-jing " ) ) . unwrap ( ) ;
std ::fs ::write (
base . join ( " .lian-jing/context.json " ) ,
r # "{"schema_version":1,"project_id":"p1"}"# ,
)
. unwrap ( ) ;
std ::fs ::create_dir_all ( base . join ( " .codex " ) ) . unwrap ( ) ;
std ::fs ::write ( base . join ( " .codex/config.toml " ) , " [mcp_servers.lian-jing] \n url = \" http://127.0.0.1:27190/mcp \" \n " ) . unwrap ( ) ;
std ::fs ::write (
base . join ( " .codex/config.toml " ) ,
" [mcp_servers.lian-jing] \n url = \" http://127.0.0.1:27190/mcp \" \n " ,
)
. unwrap ( ) ;
std ::fs ::create_dir_all ( base . join ( " .git/hooks " ) ) . unwrap ( ) ;
std ::fs ::write ( base . join ( " .git/hooks/pre-commit " ) , " call lefthook run pre-commit " ) . unwrap ( ) ;
std ::fs ::write (
base . join ( " .git/hooks/pre-commit " ) ,
" call lefthook run pre-commit " ,
)
. unwrap ( ) ;
std ::fs ::write (
base . join ( " .git/config " ) ,
" [remote \" origin \" ] \n \t url = https://git.example.com/o/r.git \n " ,
@ -753,19 +1063,26 @@ mod tests {
#[ test ]
fn managed_version_check_states ( ) {
let expected = extract_managed_block ( TMPL_AGENTS ) . and_then ( block_version ) . unwrap ( ) ;
assert! ( check_managed_version ( Some ( TMPL_AGENTS ) , expected ) . 0 , " 同版本应 pass " ) ;
let old = TMPL_AGENTS . replacen (
& format! ( " version= \" {expected} \" " ) ,
" version= \" 0.0.1 \" " ,
1 ,
let expected = extract_managed_block ( TMPL_AGENTS )
. and_then ( block_version )
. unwrap ( ) ;
assert! (
check_managed_version ( Some ( TMPL_AGENTS ) , expected ) . 0 ,
" 同版本应 pass "
) ;
let old = TMPL_AGENTS . replacen ( & format! ( " version= \" {expected} \" " ) , " version= \" 0.0.1 \" " , 1 ) ;
let ( pass , detail ) = check_managed_version ( Some ( & old ) , expected ) ;
assert! ( ! pass & & detail . contains ( " 0.0.1 " ) , " 旧版本应 fail 且报出版本号 " ) ;
assert! (
! pass & & detail . contains ( " 0.0.1 " ) ,
" 旧版本应 fail 且报出版本号 "
) ;
assert! ( ! check_managed_version ( None , expected ) . 0 , " 文件缺失应 fail " ) ;
assert! ( ! check_managed_version ( Some ( " 手写内容,无标记 " ) , expected ) . 0 , " 无标记应 fail " ) ;
assert! (
! check_managed_version ( Some ( " 手写内容,无标记 " ) , expected ) . 0 ,
" 无标记应 fail "
) ;
}
/// 回归:空路径登记曾把接入包写进炼境进程 cwd( src-tauri/, 2026-07-15 批量实调实锤)。
@ -775,8 +1092,7 @@ mod tests {
assert! ( err . contains ( " 拒绝分发 " ) , " 空路径应拒绝: {err} " ) ;
let err = apply_onboarding_pack ( " " . into ( ) , " p " . into ( ) ) . unwrap_err ( ) ;
assert! ( err . contains ( " 拒绝分发 " ) , " 空白路径应拒绝: {err} " ) ;
let err =
apply_onboarding_pack ( " E:/不存在的目录/xyz-404 " . into ( ) , " p " . into ( ) ) . unwrap_err ( ) ;
let err = apply_onboarding_pack ( " E:/不存在的目录/xyz-404 " . into ( ) , " p " . into ( ) ) . unwrap_err ( ) ;
assert! ( err . contains ( " 拒绝分发 " ) , " 不存在路径应拒绝: {err} " ) ;
}
@ -784,8 +1100,7 @@ mod tests {
#[ test ]
fn fully_managed_dispatch_three_states ( ) {
let base = std ::env ::temp_dir ( )
. join ( format! ( " lianjing_fm_test_ {} " , std ::process ::id ( ) ) ) ;
let base = std ::env ::temp_dir ( ) . join ( format! ( " lianjing_fm_test_ {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & base ) ;
std ::fs ::create_dir_all ( & base ) . unwrap ( ) ;
@ -804,15 +1119,18 @@ mod tests {
std ::fs ::write ( base . join ( " envoy.md " ) , " v1 旧内容 " ) . unwrap ( ) ;
let r = dispatch_fully_managed ( & base , " envoy.md " , " v2 内容 " ) ;
assert_eq! ( r . action , " updated " ) ;
assert_eq! ( std ::fs ::read_to_string ( base . join ( " envoy.md " ) ) . unwrap ( ) , " v2 内容 " ) ;
assert_eq! (
std ::fs ::read_to_string ( base . join ( " envoy.md " ) ) . unwrap ( ) ,
" v2 内容 "
) ;
let _ = std ::fs ::remove_dir_all ( & base ) ;
}
#[ test ]
fn dispatch_templates_upgrades_fully_managed_files ( ) {
let base = std ::env ::temp_dir ( )
. join ( format! ( " lianjing_fm_e2e_test_ {} " , std ::process ::id ( ) ) ) ;
let base =
std ::env ::temp_dir ( ) . join ( format! ( " lianjing_fm_e2e_test_ {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & base ) ;
std ::fs ::create_dir_all ( & base ) . unwrap ( ) ;
let pp = base . to_str ( ) . unwrap ( ) ;
@ -821,9 +1139,15 @@ mod tests {
// 模拟旧版本 envoy: 内容与当前模板不同 → 重跑接入包应覆盖为最新
std ::fs ::write ( base . join ( " .claude/commands/envoy.md " ) , " 旧版使者指令 " ) . unwrap ( ) ;
let r2 = dispatch_templates ( pp ) ;
let envoy = r2 . iter ( ) . find ( | t | t . name = = " .claude/commands/envoy.md " ) . unwrap ( ) ;
let envoy = r2
. iter ( )
. find ( | t | t . name = = " .claude/commands/envoy.md " )
. unwrap ( ) ;
assert_eq! ( envoy . action , " updated " , " 全托管文件应被升级,而非跳过 " ) ;
let gate = r2 . iter ( ) . find ( | t | t . name = = " .claude/hooks/blueprint-gate.cjs " ) . unwrap ( ) ;
let gate = r2
. iter ( )
. find ( | t | t . name = = " .claude/hooks/blueprint-gate.cjs " )
. unwrap ( ) ;
assert_eq! ( gate . action , " skipped " , " 未动过的全托管文件重跑应 skipped " ) ;
let _ = std ::fs ::remove_dir_all ( & base ) ;
@ -833,10 +1157,16 @@ mod tests {
fn lefthook_keys_extraction_and_check ( ) {
// 从内嵌模板自省:至少含 typecheck 与 commit-msg 侧检查段
let tpl_keys = lefthook_command_keys ( TMPL_LEFTHOOK ) ;
assert! ( tpl_keys . contains ( & " typecheck " . to_string ( ) ) , " 模板应含 typecheck: {tpl_keys:?} " ) ;
assert! (
tpl_keys . contains ( & " typecheck " . to_string ( ) ) ,
" 模板应含 typecheck: {tpl_keys:?} "
) ;
assert! ( tpl_keys . len ( ) > = 3 , " 模板检查段应不止一个: {tpl_keys:?} " ) ;
// 注释掉的示例段不提取
assert! ( ! tpl_keys . contains ( & " api-typecheck " . to_string ( ) ) , " 注释行不算键 " ) ;
assert! (
! tpl_keys . contains ( & " api-typecheck " . to_string ( ) ) ,
" 注释行不算键 "
) ;
// 项目文件与模板一致 → pass
let ( pass , _ ) = check_lefthook_keys ( Some ( TMPL_LEFTHOOK ) , TMPL_LEFTHOOK ) ;
@ -847,7 +1177,10 @@ mod tests {
let ( pass , detail ) = check_lefthook_keys ( Some ( partial ) , TMPL_LEFTHOOK ) ;
assert! ( ! pass ) ;
assert! ( detail . contains ( " 缺少检查段 " ) , " 应列出缺失键: {detail} " ) ;
assert! ( ! detail . contains ( " typecheck, " ) & & detail . contains ( " blueprint-gate " ) , " 已有键不在缺失列表: {detail} " ) ;
assert! (
! detail . contains ( " typecheck, " ) & & detail . contains ( " blueprint-gate " ) ,
" 已有键不在缺失列表: {detail} "
) ;
// 文件缺失 → fail
assert! ( ! check_lefthook_keys ( None , TMPL_LEFTHOOK ) . 0 ) ;
@ -910,4 +1243,3 @@ mod tests {
assert_eq! ( silent . len ( ) , 2 , " pw-b 的回传过期后也应列为沉默 " ) ;
}
}