fix(coexist): 新炼境接管项目文件后,旧炼境的模板分发、蓝图规则同步与 buff 轮询跳过该项目 #47
@ -1323,6 +1323,15 @@ pub(crate) fn sync_blueprint_rules_with(
|
||||
root: &Path,
|
||||
sink: &dyn crate::WriteSink,
|
||||
) -> Result<SyncResult, crate::WriteFailure> {
|
||||
if crate::mcp_inject::files_managed_by_next(root) {
|
||||
return Ok(SyncResult {
|
||||
success: true,
|
||||
conventions_written: false,
|
||||
claude_md_action: "skipped".into(),
|
||||
agents_md_action: "skipped".into(),
|
||||
message: crate::mcp_inject::FILES_MANAGED_BY_NEXT_SKIP.into(),
|
||||
});
|
||||
}
|
||||
let bp_dir = root.join(".blueprint");
|
||||
let project_name = root
|
||||
.file_name()
|
||||
@ -2876,6 +2885,22 @@ pub fn archive_project_docs(project_path: String) -> Result<ArchiveResult, Strin
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// 共存期防回写:项目文件已由新炼境接管时,蓝图规则同步零写入
|
||||
#[test]
|
||||
fn sync_skips_projects_managed_by_next() {
|
||||
let base = std::env::temp_dir().join(format!("lianjing_sync_guard_test_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
std::fs::create_dir_all(base.join(".lian-jing")).unwrap();
|
||||
std::fs::create_dir_all(base.join(".blueprint")).unwrap();
|
||||
std::fs::write(base.join(".lian-jing/context.json"), r#"{"project_id":"p1","files_managed_by":"lian-jing-next"}"#).unwrap();
|
||||
let result = sync_blueprint_rules_with(&base, &crate::DiskSink).unwrap();
|
||||
assert_eq!((result.agents_md_action.as_str(), result.claude_md_action.as_str()), ("skipped", "skipped"));
|
||||
assert!(!result.conventions_written);
|
||||
assert!(!base.join(".blueprint/CONVENTIONS.md").exists());
|
||||
assert!(!base.join("AGENTS.md").exists() && !base.join("CLAUDE.md").exists());
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
use rusqlite::Connection;
|
||||
use std::fs;
|
||||
|
||||
|
||||
@ -125,6 +125,10 @@ fn poll_all_buffs(app: &AppHandle) {
|
||||
|
||||
fn check_project(app: &AppHandle, project_path: &str) -> Result<(), String> {
|
||||
let root = Path::new(project_path);
|
||||
// 共存期防回写:项目文件已由新炼境接管,不再改写其任务卡
|
||||
if crate::mcp_inject::files_managed_by_next(root) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 获取当前 HEAD commit
|
||||
let current_commit = match get_head_commit(root) {
|
||||
|
||||
@ -199,6 +199,14 @@ pub(crate) fn dispatch_templates_with(
|
||||
root: &Path,
|
||||
sink: &dyn crate::WriteSink,
|
||||
) -> Vec<TemplateResult> {
|
||||
if crate::mcp_inject::files_managed_by_next(root) {
|
||||
return vec![TemplateResult {
|
||||
name: ".lian-jing/context.json".into(),
|
||||
action: "skipped".into(),
|
||||
detail: crate::mcp_inject::FILES_MANAGED_BY_NEXT_SKIP.into(),
|
||||
warning: None,
|
||||
}];
|
||||
}
|
||||
vec![
|
||||
dispatch_versioned(root, "AGENTS.md", TMPL_AGENTS, sink),
|
||||
dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK, sink),
|
||||
@ -1243,6 +1251,21 @@ pub fn silent_projects(conn: &rusqlite::Connection) -> Result<Vec<(String, i64)>
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// 共存期防回写:项目文件已由新炼境接管时,接入包模板分发零写入
|
||||
#[test]
|
||||
fn dispatch_skips_projects_managed_by_next() {
|
||||
let base = std::env::temp_dir().join(format!("lianjing_dispatch_guard_test_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
std::fs::create_dir_all(base.join(".lian-jing")).unwrap();
|
||||
std::fs::create_dir_all(base.join(".blueprint")).unwrap();
|
||||
std::fs::write(base.join(".lian-jing/context.json"), r#"{"project_id":"p1","files_managed_by":"lian-jing-next"}"#).unwrap();
|
||||
let results = super::dispatch_templates(base.to_str().unwrap());
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].action, "skipped");
|
||||
assert!(!base.join("AGENTS.md").exists() && !base.join(".claude").exists() && !base.join("lefthook.yml").exists());
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@ -235,6 +235,20 @@ fn write_json_with(
|
||||
.map_err(|error| format!("写入 {label} 失败: {error}"))
|
||||
}
|
||||
|
||||
/// 新炼境(lian-jing-next)接管项目文件后写入 `.lian-jing/context.json` 的 `files_managed_by` 取值。
|
||||
pub(crate) const FILES_MANAGED_BY_NEXT: &str = "lian-jing-next";
|
||||
|
||||
/// 共存期防回写守卫:项目文件已由新炼境接管时,旧炼境的模板分发、蓝图规则同步与 buff 轮询一律跳过,
|
||||
/// 避免两边来回覆盖同一批托管文件。只读判断,文件缺失或不合法时视为未接管。
|
||||
pub(crate) fn files_managed_by_next(root: &std::path::Path) -> bool {
|
||||
std::fs::read_to_string(root.join(".lian-jing").join("context.json"))
|
||||
.ok()
|
||||
.and_then(|text| serde_json::from_str::<Value>(&text).ok())
|
||||
.is_some_and(|ctx| ctx.get("files_managed_by").and_then(Value::as_str) == Some(FILES_MANAGED_BY_NEXT))
|
||||
}
|
||||
|
||||
pub(crate) const FILES_MANAGED_BY_NEXT_SKIP: &str = "项目文件已由新炼境接管(.lian-jing/context.json 的 files_managed_by),旧炼境不再分发";
|
||||
|
||||
/// 写工具无关的项目身份锚点。项目根目录是边界,客户端项目 UI 或用户级配置均不是身份来源。
|
||||
/// 合并写入:保留未来字段与项目定制字段,只有炼境拥有的身份字段会被刷新。
|
||||
#[cfg(test)]
|
||||
@ -3623,6 +3637,26 @@ trust_level = "trusted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_managed_by_next_guard_reads_context_and_survives_identity_rewrite() {
|
||||
let base = std::env::temp_dir().join(format!("lianjing_files_guard_test_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
std::fs::create_dir_all(base.join(".lian-jing")).unwrap();
|
||||
assert!(!files_managed_by_next(&base), "没有 context.json 视为未接管");
|
||||
std::fs::write(
|
||||
base.join(".lian-jing/context.json"),
|
||||
r#"{"project_id":"p1","schema_version":1,"files_managed_by":"lian-jing-next"}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(files_managed_by_next(&base));
|
||||
// 旧炼境刷新身份时必须保留接管标记,否则守卫会被自己抹掉
|
||||
write_canonical_context_at(&base, "p1").unwrap();
|
||||
assert!(files_managed_by_next(&base), "身份合并写后接管标记仍在");
|
||||
std::fs::write(base.join(".lian-jing/context.json"), r#"{"project_id":"p1","files_managed_by":"other"}"#).unwrap();
|
||||
assert!(!files_managed_by_next(&base));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_context_migrates_legacy_project_id_and_preserves_unknown_fields() {
|
||||
let base = std::env::temp_dir().join(format!(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user