feat(onboarding): add capability pack status
Feature-Confirmed: true
This commit is contained in:
parent
167fcf9880
commit
122864f340
@ -33,12 +33,13 @@ Codex 用户级插件
|
|||||||
- acceptance: "[cargo test] 对仅含 `.claude/liangjing.json` 的临时项目执行接入/验证 → 写入 `.lian-jing/context.json` 且 project_id 一致;已有 context 的未知字段保留;手工 `[mcp_servers.lian-jing]` 配置仍拒绝覆盖。"
|
- acceptance: "[cargo test] 对仅含 `.claude/liangjing.json` 的临时项目执行接入/验证 → 写入 `.lian-jing/context.json` 且 project_id 一致;已有 context 的未知字段保留;手工 `[mcp_servers.lian-jing]` 配置仍拒绝覆盖。"
|
||||||
- done_note: "2026-09-21:inject_mcp 先写 `.lian-jing/context.json`(schema_version=1、project_id),再写 Codex/Claude 投影;canonical 文件保留未知字段,旧 Claude context 继续保留运行时端口。"
|
- done_note: "2026-09-21:inject_mcp 先写 `.lian-jing/context.json`(schema_version=1、project_id),再写 Codex/Claude 投影;canonical 文件保留未知字段,旧 Claude context 继续保留运行时端口。"
|
||||||
|
|
||||||
### 📋 P2-B. 能力包描述符与 onboarding 自检报告
|
### ✅ P2-B. 能力包描述符与 onboarding 自检报告
|
||||||
- status: todo
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- depends: P2-A. 工具无关的 canonical 项目上下文与兼容迁移
|
- depends: P2-A. 工具无关的 canonical 项目上下文与兼容迁移
|
||||||
- files: src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs, src-tauri/resources/onboarding/, docs/ai-context/project-context.yaml
|
- files: src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs, src-tauri/resources/onboarding/, docs/ai-context/project-context.yaml
|
||||||
- acceptance: "[cargo test] verify_onboarding(project_id) → 对 canonical 缺失、legacy-only、pack 版本不符、Codex MCP 不可用分别返回可区分的 ready/missing/legacy/version_mismatch/unavailable 信号,且不将任一状态伪报为 ready。"
|
- acceptance: "[cargo test] verify_onboarding(project_id) → 对 canonical 缺失、legacy-only、pack 版本不符、Codex MCP 不可用分别返回可区分的 ready/missing/legacy/version_mismatch/unavailable 信号,且不将任一状态伪报为 ready。"
|
||||||
|
- done_note: "2026-09-21:新增 `.lian-jing/capability-packs.json` 基线描述符;verify_onboarding 对项目上下文、能力包、Codex skill/MCP 和既有基线逐项返回结构化状态与可执行升级提示。"
|
||||||
|
|
||||||
### 📋 P2-C. Codex Skill-only 可移植能力包
|
### 📋 P2-C. Codex Skill-only 可移植能力包
|
||||||
- status: todo
|
- status: todo
|
||||||
|
|||||||
@ -18,9 +18,11 @@ contracts:
|
|||||||
- label: 被管理项目身份上下文
|
- label: 被管理项目身份上下文
|
||||||
paths:
|
paths:
|
||||||
- "src-tauri/src/mcp_inject.rs"
|
- "src-tauri/src/mcp_inject.rs"
|
||||||
|
- "src-tauri/src/commands/onboarding.rs"
|
||||||
- ".lian-jing/context.json"
|
- ".lian-jing/context.json"
|
||||||
|
- ".lian-jing/capability-packs.json"
|
||||||
- ".claude/liangjing.json"
|
- ".claude/liangjing.json"
|
||||||
doc: "`.lian-jing/context.json` 是 project_id 的工具无关单一事实来源;`.claude/liangjing.json` 仅为兼容投影。迁移必须保留未知字段,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。"
|
doc: "`.lian-jing/context.json` 是 project_id 的工具无关单一事实来源;`.claude/liangjing.json` 仅为兼容投影;`.lian-jing/capability-packs.json` 是炼境分发基线的版本描述。迁移必须保留未知字段,验证必须返回显式状态,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。"
|
||||||
|
|
||||||
- label: Tauri command 接口(前后端契约)
|
- label: Tauri command 接口(前后端契约)
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"packs": [
|
||||||
|
{
|
||||||
|
"pack_id": "base-onboarding",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"supported_clients": ["codex", "claude"],
|
||||||
|
"project_artifacts": [
|
||||||
|
"AGENTS.md",
|
||||||
|
".agents/skills/lian-jing/SKILL.md",
|
||||||
|
".lian-jing/context.json"
|
||||||
|
],
|
||||||
|
"self_check": "verify_onboarding",
|
||||||
|
"upgrade_hint": "调用 apply_onboarding_pack(project_id) 更新基线;项目身份上下文由 inject_mcp(project_id) 创建或迁移。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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_LEFTHOOK: &str = include_str!("../../resources/onboarding/lefthook.yml.tmpl");
|
||||||
const TMPL_GIT_WORKFLOW: &str = include_str!("../../resources/onboarding/git-workflow.md.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_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_BLUEPRINT_GATE: &str =
|
||||||
const TMPL_LIANJING_SKILL: &str = include_str!("../../resources/onboarding/.agents/skills/lian-jing/SKILL.md");
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
@ -134,9 +141,8 @@ fn dispatch_versioned(root: &Path, relative: &str, template: &str) -> TemplateRe
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (new_content, action, detail) = if let (Some(s), Some(e)) =
|
let (new_content, action, detail) =
|
||||||
(content.find(MANAGED_START), content.find(MANAGED_END))
|
if let (Some(s), Some(e)) = (content.find(MANAGED_START), content.find(MANAGED_END)) {
|
||||||
{
|
|
||||||
let old_ver = block_version(&content).unwrap_or("?").to_string();
|
let old_ver = block_version(&content).unwrap_or("?").to_string();
|
||||||
if old_ver == tpl_ver {
|
if old_ver == tpl_ver {
|
||||||
return TemplateResult {
|
return TemplateResult {
|
||||||
@ -149,7 +155,11 @@ fn dispatch_versioned(root: &Path, relative: &str, template: &str) -> TemplateRe
|
|||||||
nc.push_str(&content[..s]);
|
nc.push_str(&content[..s]);
|
||||||
nc.push_str(tpl_block);
|
nc.push_str(tpl_block);
|
||||||
nc.push_str(&content[e + MANAGED_END.len()..]);
|
nc.push_str(&content[e + MANAGED_END.len()..]);
|
||||||
(nc, "updated", format!("管理块 v{old_ver} → v{tpl_ver},块外内容未动"))
|
(
|
||||||
|
nc,
|
||||||
|
"updated",
|
||||||
|
format!("管理块 v{old_ver} → v{tpl_ver},块外内容未动"),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
format!("{}\n\n{}\n", content.trim_end(), tpl_block),
|
format!("{}\n\n{}\n", content.trim_end(), tpl_block),
|
||||||
@ -181,8 +191,21 @@ pub fn dispatch_templates(project_path: &str) -> Vec<TemplateResult> {
|
|||||||
dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK),
|
dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK),
|
||||||
dispatch_versioned(root, "docs/ai-context/git-workflow.md", TMPL_GIT_WORKFLOW),
|
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/commands/envoy.md", TMPL_ENVOY),
|
||||||
dispatch_fully_managed(root, ".claude/hooks/blueprint-gate.cjs", TMPL_BLUEPRINT_GATE),
|
dispatch_fully_managed(
|
||||||
dispatch_fully_managed(root, ".agents/skills/lian-jing/SKILL.md", TMPL_LIANJING_SKILL),
|
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 {
|
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() => {
|
Ok(out) if out.status.success() => {
|
||||||
return HookActivation {
|
return HookActivation {
|
||||||
status: "activated".into(),
|
status: "activated".into(),
|
||||||
@ -275,7 +302,11 @@ pub fn apply_onboarding_pack(
|
|||||||
let templates = dispatch_templates(&project_path);
|
let templates = dispatch_templates(&project_path);
|
||||||
let hook = run_lefthook_install(Path::new(project_path.trim_end_matches(['/', '\\'])));
|
let hook = run_lefthook_install(Path::new(project_path.trim_end_matches(['/', '\\'])));
|
||||||
let buff = crate::commands::buff::apply_blueprint_buff(project_path, project_id)?;
|
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 检查链,判定逻辑纯函数化可单测)──────
|
// ── V1:接入生效验收(verify_onboarding 检查链,判定逻辑纯函数化可单测)──────
|
||||||
@ -285,9 +316,170 @@ pub fn apply_onboarding_pack(
|
|||||||
pub struct VerifyCheck {
|
pub struct VerifyCheck {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub pass: bool,
|
pub pass: bool,
|
||||||
|
pub state: VerifyState,
|
||||||
pub detail: String,
|
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 接管。
|
/// 纯函数:pre-commit hook 内容是否由 lefthook 接管。
|
||||||
fn hook_is_lefthook(content: &str) -> bool {
|
fn hook_is_lefthook(content: &str) -> bool {
|
||||||
content.contains("lefthook")
|
content.contains("lefthook")
|
||||||
@ -299,7 +491,9 @@ fn parse_origin_url(git_config: &str) -> Option<String> {
|
|||||||
for line in git_config.lines() {
|
for line in git_config.lines() {
|
||||||
let t = line.trim();
|
let t = line.trim();
|
||||||
if t.starts_with('[') {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
if in_origin {
|
if in_origin {
|
||||||
@ -340,15 +534,24 @@ fn check_codex_mcp(content: Option<&str>) -> (bool, String) {
|
|||||||
{
|
{
|
||||||
(true, "lian-jing 指向本地 MCP 服务".into())
|
(true, "lian-jing 指向本地 MCP 服务".into())
|
||||||
}
|
}
|
||||||
Some(_) => (false, "未找到炼境的 Codex MCP 配置——调用 inject_mcp 修复".into()),
|
Some(_) => (
|
||||||
None => (false, ".codex/config.toml 缺失——调用 inject_mcp 后生成".into()),
|
false,
|
||||||
|
"未找到炼境的 Codex MCP 配置——调用 inject_mcp 修复".into(),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
false,
|
||||||
|
".codex/config.toml 缺失——调用 inject_mcp 后生成".into(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_skill(content: Option<&str>) -> (bool, String) {
|
fn check_skill(content: Option<&str>) -> (bool, String) {
|
||||||
match content {
|
match content {
|
||||||
Some(content) if content.contains("name: lian-jing") => (true, "炼境项目技能已分发".into()),
|
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) {
|
fn check_managed_version(content: Option<&str>, expected: &str) -> (bool, String) {
|
||||||
match content.and_then(block_version) {
|
match content.and_then(block_version) {
|
||||||
Some(v) if v == expected => (true, format!("管理块 v{v}(最新)")),
|
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()),
|
None => (false, "文件缺失或无管理块标记".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -373,7 +579,10 @@ fn check_origin(origin_host: Option<&str>, instance_hosts: &[String]) -> (bool,
|
|||||||
}
|
}
|
||||||
Some(h) => (
|
Some(h) => (
|
||||||
false,
|
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)。
|
/// conn 由调用方注入(R06)。
|
||||||
pub fn verify_onboarding(
|
pub fn verify_onboarding(
|
||||||
conn: &rusqlite::Connection,
|
conn: &rusqlite::Connection,
|
||||||
@ -449,27 +658,44 @@ pub fn verify_onboarding(
|
|||||||
"git-workflow 管理块版本(含交付状态调用说明)",
|
"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);
|
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());
|
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());
|
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
|
// 3 lefthook hook 真挂在 .git/hooks
|
||||||
let (pass, detail) = match read(".git/hooks/pre-commit") {
|
let (pass, detail) = match read(".git/hooks/pre-commit") {
|
||||||
Some(c) if hook_is_lefthook(&c) => (true, "pre-commit 由 lefthook 接管".to_string()),
|
Some(c) if hook_is_lefthook(&c) => (true, "pre-commit 由 lefthook 接管".to_string()),
|
||||||
Some(_) => (false, "pre-commit 存在但非 lefthook(被其他工具占用?)".to_string()),
|
Some(_) => (
|
||||||
None => (false, "无 pre-commit hook——lefthook install 未执行".to_string()),
|
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)
|
// 4 lefthook 检查段齐全(期望键从内嵌模板自省,U1)
|
||||||
let (pass, detail) = check_lefthook_keys(read("lefthook.yml").as_deref(), TMPL_LEFTHOOK);
|
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 实例
|
// 5 origin 指向已登记 Gitea 实例
|
||||||
let origin_host = read(".git/config")
|
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);
|
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 存在
|
// 6 CI workflow 存在
|
||||||
let yml_count = std::fs::read_dir(root.join(".gitea/workflows"))
|
let yml_count = std::fs::read_dir(root.join(".gitea/workflows"))
|
||||||
@ -503,15 +729,15 @@ pub fn verify_onboarding(
|
|||||||
.count()
|
.count()
|
||||||
})
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
checks.push(VerifyCheck {
|
checks.push(VerifyCheck::from_pass(
|
||||||
name: "CI workflow 存在".into(),
|
"CI workflow 存在",
|
||||||
pass: yml_count > 0,
|
yml_count > 0,
|
||||||
detail: if yml_count > 0 {
|
if yml_count > 0 {
|
||||||
format!(".gitea/workflows/ 有 {yml_count} 个 workflow")
|
format!(".gitea/workflows/ 有 {yml_count} 个 workflow")
|
||||||
} else {
|
} else {
|
||||||
"无 .gitea/workflows/*.yml——可调 distribute_cicd 分发".into()
|
"无 .gitea/workflows/*.yml——可调 distribute_cicd 分发".into()
|
||||||
},
|
},
|
||||||
});
|
));
|
||||||
|
|
||||||
// 7 飞轮数据在流入(近 30 天 commit_metrics)
|
// 7 飞轮数据在流入(近 30 天 commit_metrics)
|
||||||
let recent: i64 = conn
|
let recent: i64 = conn
|
||||||
@ -521,15 +747,15 @@ pub fn verify_onboarding(
|
|||||||
|r| r.get(0),
|
|r| r.get(0),
|
||||||
)
|
)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
checks.push(VerifyCheck {
|
checks.push(VerifyCheck::from_pass(
|
||||||
name: "飞轮 commit 数据流入".into(),
|
"飞轮 commit 数据流入",
|
||||||
pass: recent > 0,
|
recent > 0,
|
||||||
detail: if recent > 0 {
|
if recent > 0 {
|
||||||
format!("近 30 天 {recent} 条 commit 已采集")
|
format!("近 30 天 {recent} 条 commit 已采集")
|
||||||
} else {
|
} else {
|
||||||
"近 30 天无数据——确认 buff 已挂且有 commit 活动(或调 poll_now)".into()
|
"近 30 天无数据——确认 buff 已挂且有 commit 活动(或调 poll_now)".into()
|
||||||
},
|
},
|
||||||
});
|
));
|
||||||
|
|
||||||
checks
|
checks
|
||||||
}
|
}
|
||||||
@ -577,8 +803,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn writes_then_skips_never_overwrites() {
|
fn writes_then_skips_never_overwrites() {
|
||||||
let base = std::env::temp_dir()
|
let base =
|
||||||
.join(format!("lianjing_onboarding_test_{}", std::process::id()));
|
std::env::temp_dir().join(format!("lianjing_onboarding_test_{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
std::fs::create_dir_all(&base).unwrap();
|
std::fs::create_dir_all(&base).unwrap();
|
||||||
let pp = base.to_str().unwrap();
|
let pp = base.to_str().unwrap();
|
||||||
@ -608,8 +834,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn versioned_dispatch_three_states() {
|
fn versioned_dispatch_three_states() {
|
||||||
let base = std::env::temp_dir()
|
let base =
|
||||||
.join(format!("lianjing_onb_ver_test_{}", std::process::id()));
|
std::env::temp_dir().join(format!("lianjing_onb_ver_test_{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
std::fs::create_dir_all(&base).unwrap();
|
std::fs::create_dir_all(&base).unwrap();
|
||||||
let pp = base.to_str().unwrap();
|
let pp = base.to_str().unwrap();
|
||||||
@ -631,13 +857,13 @@ mod tests {
|
|||||||
.and_then(block_version)
|
.and_then(block_version)
|
||||||
.expect("模板必须带版本标记");
|
.expect("模板必须带版本标记");
|
||||||
let old = agents
|
let old = agents
|
||||||
|
.replace(&format!("version=\"{tpl_ver}\""), "version=\"1.0.0\"")
|
||||||
.replace(
|
.replace(
|
||||||
&format!("version=\"{tpl_ver}\""),
|
"## 项目概况",
|
||||||
"version=\"1.0.0\"",
|
"## 项目概况
|
||||||
)
|
|
||||||
.replace("## 项目概况", "## 项目概况
|
|
||||||
|
|
||||||
我的项目说明")
|
我的项目说明",
|
||||||
|
)
|
||||||
+ "
|
+ "
|
||||||
## 用户自加的章节
|
## 用户自加的章节
|
||||||
内容
|
内容
|
||||||
@ -651,14 +877,24 @@ mod tests {
|
|||||||
upgraded.contains(&format!("version=\"{tpl_ver}\"")),
|
upgraded.contains(&format!("version=\"{tpl_ver}\"")),
|
||||||
"块应升级到模板版本"
|
"块应升级到模板版本"
|
||||||
);
|
);
|
||||||
assert!(upgraded.contains("我的项目说明"), "块外用户内容(前)须保留");
|
assert!(
|
||||||
assert!(upgraded.contains("用户自加的章节"), "块外用户内容(后)须保留");
|
upgraded.contains("我的项目说明"),
|
||||||
|
"块外用户内容(前)须保留"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
upgraded.contains("用户自加的章节"),
|
||||||
|
"块外用户内容(后)须保留"
|
||||||
|
);
|
||||||
assert_eq!(upgraded.matches(MANAGED_START).count(), 1, "只有一个管理块");
|
assert_eq!(upgraded.matches(MANAGED_START).count(), 1, "只有一个管理块");
|
||||||
|
|
||||||
// 态三:存量文件无标记 → appended,原内容保留在上方
|
// 态三:存量文件无标记 → 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 r4 = dispatch_templates(pp);
|
||||||
let agents_r = r4.iter().find(|t| t.name == "AGENTS.md").unwrap();
|
let agents_r = r4.iter().find(|t| t.name == "AGENTS.md").unwrap();
|
||||||
assert_eq!(agents_r.action, "appended");
|
assert_eq!(agents_r.action, "appended");
|
||||||
@ -677,8 +913,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hook_detection_covers_mounted_and_foreign() {
|
fn hook_detection_covers_mounted_and_foreign() {
|
||||||
assert!(hook_is_lefthook("#!/bin/sh\ncall lefthook run pre-commit"), "lefthook 接管应判 true");
|
assert!(
|
||||||
assert!(!hook_is_lefthook("#!/bin/sh\nnpx husky run"), "非 lefthook hook 应判 false");
|
hook_is_lefthook("#!/bin/sh\ncall lefthook run pre-commit"),
|
||||||
|
"lefthook 接管应判 true"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!hook_is_lefthook("#!/bin/sh\nnpx husky run"),
|
||||||
|
"非 lefthook hook 应判 false"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -689,41 +931,109 @@ mod tests {
|
|||||||
Some("https://git.example.com/o/r.git"),
|
Some("https://git.example.com/o/r.git"),
|
||||||
"应取 origin 而非其他 remote"
|
"应取 origin 而非其他 remote"
|
||||||
);
|
);
|
||||||
assert_eq!(parse_origin_url("[core]\n\tbare = false"), None, "无 origin 应返回 None");
|
assert_eq!(
|
||||||
|
parse_origin_url("[core]\n\tbare = false"),
|
||||||
|
None,
|
||||||
|
"无 origin 应返回 None"
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(url_host("https://git.example.com/o/r.git").as_deref(), Some("git.example.com"));
|
assert_eq!(
|
||||||
assert_eq!(url_host("https://user@git.example.com:3000/o/r.git").as_deref(), Some("git.example.com"), "应忽略用户名与端口");
|
url_host("https://git.example.com/o/r.git").as_deref(),
|
||||||
assert_eq!(url_host("git@git.example.com:o/r.git").as_deref(), Some("git.example.com"), "ssh 形式");
|
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]
|
#[test]
|
||||||
fn origin_check_match_and_mismatch() {
|
fn origin_check_match_and_mismatch() {
|
||||||
let hosts = vec!["git.example.com".to_string()];
|
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(Some("github.com"), &hosts).0, "未命中应 fail");
|
||||||
assert!(!check_origin(None, &hosts).0, "无 origin 应 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]
|
#[test]
|
||||||
fn verify_onboarding_full_chain() {
|
fn verify_onboarding_full_chain() {
|
||||||
let conn = crate::db::conn_with_schema();
|
let conn = crate::db::conn_with_schema();
|
||||||
let base = std::env::temp_dir()
|
let base =
|
||||||
.join(format!("lianjing_verify_test_{}", std::process::id()));
|
std::env::temp_dir().join(format!("lianjing_verify_test_{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
std::fs::create_dir_all(&base).unwrap();
|
std::fs::create_dir_all(&base).unwrap();
|
||||||
|
|
||||||
// 空目录:七项全不通过(真实反映"什么都没接")
|
// 空目录:所有检查均不通过(真实反映"什么都没接")
|
||||||
let checks = verify_onboarding(&conn, "p1", &base);
|
let checks = verify_onboarding(&conn, "p1", &base);
|
||||||
assert_eq!(checks.len(), 9, "检查链应为九项");
|
assert_eq!(checks.len(), 11, "检查链应为十一项");
|
||||||
assert!(checks.iter().all(|c| !c.pass), "空项目应全部不通过");
|
assert!(checks.iter().all(|c| !c.pass), "空项目应全部不通过");
|
||||||
|
|
||||||
// 铺齐全部生效条件后:七项全通过
|
// 铺齐全部生效条件后:所有检查全通过
|
||||||
dispatch_templates(base.to_str().unwrap());
|
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::create_dir_all(base.join(".codex")).unwrap();
|
||||||
std::fs::write(base.join(".codex/config.toml"), "[mcp_servers.lian-jing]\nurl = \"http://127.0.0.1:27190/mcp\"\n").unwrap();
|
std::fs::write(
|
||||||
|
base.join(".codex/config.toml"),
|
||||||
|
"[mcp_servers.lian-jing]\nurl = \"http://127.0.0.1:27190/mcp\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
std::fs::create_dir_all(base.join(".git/hooks")).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(
|
std::fs::write(
|
||||||
base.join(".git/config"),
|
base.join(".git/config"),
|
||||||
"[remote \"origin\"]\n\turl = https://git.example.com/o/r.git\n",
|
"[remote \"origin\"]\n\turl = https://git.example.com/o/r.git\n",
|
||||||
@ -753,19 +1063,26 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn managed_version_check_states() {
|
fn managed_version_check_states() {
|
||||||
let expected = extract_managed_block(TMPL_AGENTS).and_then(block_version).unwrap();
|
let expected = extract_managed_block(TMPL_AGENTS)
|
||||||
assert!(check_managed_version(Some(TMPL_AGENTS), expected).0, "同版本应 pass");
|
.and_then(block_version)
|
||||||
|
.unwrap();
|
||||||
let old = TMPL_AGENTS.replacen(
|
assert!(
|
||||||
&format!("version=\"{expected}\""),
|
check_managed_version(Some(TMPL_AGENTS), expected).0,
|
||||||
"version=\"0.0.1\"",
|
"同版本应 pass"
|
||||||
1,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let old = TMPL_AGENTS.replacen(&format!("version=\"{expected}\""), "version=\"0.0.1\"", 1);
|
||||||
let (pass, detail) = check_managed_version(Some(&old), expected);
|
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(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 批量实调实锤)。
|
/// 回归:空路径登记曾把接入包写进炼境进程 cwd(src-tauri/,2026-07-15 批量实调实锤)。
|
||||||
@ -775,8 +1092,7 @@ mod tests {
|
|||||||
assert!(err.contains("拒绝分发"), "空路径应拒绝: {err}");
|
assert!(err.contains("拒绝分发"), "空路径应拒绝: {err}");
|
||||||
let err = apply_onboarding_pack(" ".into(), "p".into()).unwrap_err();
|
let err = apply_onboarding_pack(" ".into(), "p".into()).unwrap_err();
|
||||||
assert!(err.contains("拒绝分发"), "空白路径应拒绝: {err}");
|
assert!(err.contains("拒绝分发"), "空白路径应拒绝: {err}");
|
||||||
let err =
|
let err = apply_onboarding_pack("E:/不存在的目录/xyz-404".into(), "p".into()).unwrap_err();
|
||||||
apply_onboarding_pack("E:/不存在的目录/xyz-404".into(), "p".into()).unwrap_err();
|
|
||||||
assert!(err.contains("拒绝分发"), "不存在路径应拒绝: {err}");
|
assert!(err.contains("拒绝分发"), "不存在路径应拒绝: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -784,8 +1100,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fully_managed_dispatch_three_states() {
|
fn fully_managed_dispatch_three_states() {
|
||||||
let base = std::env::temp_dir()
|
let base = std::env::temp_dir().join(format!("lianjing_fm_test_{}", std::process::id()));
|
||||||
.join(format!("lianjing_fm_test_{}", std::process::id()));
|
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
std::fs::create_dir_all(&base).unwrap();
|
std::fs::create_dir_all(&base).unwrap();
|
||||||
|
|
||||||
@ -804,15 +1119,18 @@ mod tests {
|
|||||||
std::fs::write(base.join("envoy.md"), "v1 旧内容").unwrap();
|
std::fs::write(base.join("envoy.md"), "v1 旧内容").unwrap();
|
||||||
let r = dispatch_fully_managed(&base, "envoy.md", "v2 内容");
|
let r = dispatch_fully_managed(&base, "envoy.md", "v2 内容");
|
||||||
assert_eq!(r.action, "updated");
|
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);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dispatch_templates_upgrades_fully_managed_files() {
|
fn dispatch_templates_upgrades_fully_managed_files() {
|
||||||
let base = std::env::temp_dir()
|
let base =
|
||||||
.join(format!("lianjing_fm_e2e_test_{}", std::process::id()));
|
std::env::temp_dir().join(format!("lianjing_fm_e2e_test_{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
std::fs::create_dir_all(&base).unwrap();
|
std::fs::create_dir_all(&base).unwrap();
|
||||||
let pp = base.to_str().unwrap();
|
let pp = base.to_str().unwrap();
|
||||||
@ -821,9 +1139,15 @@ mod tests {
|
|||||||
// 模拟旧版本 envoy:内容与当前模板不同 → 重跑接入包应覆盖为最新
|
// 模拟旧版本 envoy:内容与当前模板不同 → 重跑接入包应覆盖为最新
|
||||||
std::fs::write(base.join(".claude/commands/envoy.md"), "旧版使者指令").unwrap();
|
std::fs::write(base.join(".claude/commands/envoy.md"), "旧版使者指令").unwrap();
|
||||||
let r2 = dispatch_templates(pp);
|
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", "全托管文件应被升级,而非跳过");
|
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");
|
assert_eq!(gate.action, "skipped", "未动过的全托管文件重跑应 skipped");
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
@ -833,10 +1157,16 @@ mod tests {
|
|||||||
fn lefthook_keys_extraction_and_check() {
|
fn lefthook_keys_extraction_and_check() {
|
||||||
// 从内嵌模板自省:至少含 typecheck 与 commit-msg 侧检查段
|
// 从内嵌模板自省:至少含 typecheck 与 commit-msg 侧检查段
|
||||||
let tpl_keys = lefthook_command_keys(TMPL_LEFTHOOK);
|
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.len() >= 3, "模板检查段应不止一个: {tpl_keys:?}");
|
||||||
// 注释掉的示例段不提取
|
// 注释掉的示例段不提取
|
||||||
assert!(!tpl_keys.contains(&"api-typecheck".to_string()), "注释行不算键");
|
assert!(
|
||||||
|
!tpl_keys.contains(&"api-typecheck".to_string()),
|
||||||
|
"注释行不算键"
|
||||||
|
);
|
||||||
|
|
||||||
// 项目文件与模板一致 → pass
|
// 项目文件与模板一致 → pass
|
||||||
let (pass, _) = check_lefthook_keys(Some(TMPL_LEFTHOOK), TMPL_LEFTHOOK);
|
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);
|
let (pass, detail) = check_lefthook_keys(Some(partial), TMPL_LEFTHOOK);
|
||||||
assert!(!pass);
|
assert!(!pass);
|
||||||
assert!(detail.contains("缺少检查段"), "应列出缺失键: {detail}");
|
assert!(detail.contains("缺少检查段"), "应列出缺失键: {detail}");
|
||||||
assert!(!detail.contains("typecheck,") && detail.contains("blueprint-gate"), "已有键不在缺失列表: {detail}");
|
assert!(
|
||||||
|
!detail.contains("typecheck,") && detail.contains("blueprint-gate"),
|
||||||
|
"已有键不在缺失列表: {detail}"
|
||||||
|
);
|
||||||
|
|
||||||
// 文件缺失 → fail
|
// 文件缺失 → fail
|
||||||
assert!(!check_lefthook_keys(None, TMPL_LEFTHOOK).0);
|
assert!(!check_lefthook_keys(None, TMPL_LEFTHOOK).0);
|
||||||
@ -910,4 +1243,3 @@ mod tests {
|
|||||||
assert_eq!(silent.len(), 2, "pw-b 的回传过期后也应列为沉默");
|
assert_eq!(silent.len(), 2, "pw-b 的回传过期后也应列为沉默");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -262,7 +262,7 @@ pub fn tools_list_result() -> Value {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "verify_onboarding",
|
"name": "verify_onboarding",
|
||||||
"description": "接入生效验收:检查接入包在项目里是否真正生效(含 get_delivery_status 调用说明的模板管理块版本、lefthook hook 真挂在 .git/hooks、检查段对齐模板、origin 指向已登记 Gitea、CI workflow 存在、近 30 天飞轮 commit 数据流入)。分发成功 ≠ 功能生效,apply_onboarding_pack 之后应调用本工具做消费者角色验收。",
|
"description": "接入生效验收:检查接入包在项目里是否真正生效(含工具无关项目上下文、基础能力包版本、Codex MCP/skill、模板管理块、lefthook、origin、CI 与飞轮数据)。每项返回 ready / missing / legacy / version_mismatch / unavailable 的真实状态和升级提示;分发成功 ≠ 功能生效,apply_onboarding_pack 或 inject_mcp 之后应调用本工具验收。",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user