fix(mcp-inject): 「已接入」统一为有项目 token 单一判定,批量重注入补上独立接入项目(P8a)
All checks were successful
Push & PR Check / check (push) Successful in 1m2s
All checks were successful
Push & PR Check / check (push) Successful in 1m2s
「已接入」原有三套判定:reinject_all_mcp 按产品组成员、DSH 白名单与根目录迁移按
项目 token、list_all_projects 按 .claude/liangjing.json 是否存在。结果是独立接入
(不在任何组)的项目在端口变更后不会被重注入,list_all_projects 对只剩兼容文件的
项目误报「已注入」。
- 新增 mcp_inject::is_project_injected(store, project_id),DSH 白名单、
on_project_root_changed、list_all_projects 统一调用
- reinject_all_mcp 目标改为「组成员 ∪ 有 token 的登记项目」(reinject_targets),
锁内复查改为 is_reinject_target,token 先被删的项目不再注入(语义不变)
- list_all_projects 拆出 render_all_projects(conn, store),去掉循环内二次取池
- 新增两条测试,变异确认会失败;契约同步 project-context.yaml;蓝图 P8a 标 ✅
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f8dc95c6ee
commit
8230910358
@ -511,9 +511,9 @@ modules:
|
||||
name: 三端客户端对等
|
||||
area: infra
|
||||
status: in_progress
|
||||
progress: 35
|
||||
progress: 39
|
||||
updated: 2026-10-03
|
||||
note: "P1/P2/P3/P3b/P7/P9/P10 完成:三端(Claude Code、Codex 含 Desktop、DSH 含桌面 GUI)在 Enterprise 经 %USERPROFILE%\\.lian-jing 稳定入口连项目专用地址,人终验通过。Claude 条目经 claude CLI 写入、以 claude mcp get 为准;DSH 由托管块全局行(headless)+ 会话级插件(GUI/web/acp 按会话工作目录)路由;接入写入幂等;接入包改写块外带未提交改动的托管块文件时给出混合文件预警;P7 实测 MCP 服务器说明仅 Claude/DSH 带入模型上下文、Codex 不带,引导提示保留在 skill。2026-10-03 P8 一键接入设计定稿(docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md):A1 不前置;P4 分 L0 静态 / L1 零模型 / L3 无头会话,P8 只依赖 L1(P4a);拆为 P8-0、P8a–P8g 与 P4-0、P4a–P4e(共 13 张待做 S/M 卡),P8 进行中;进度按拆分后卡数重算。待做 P5、P6(基于 P8c 的 WriteSink)。"
|
||||
note: "P1/P2/P3/P3b/P7/P9/P10 完成:三端(Claude Code、Codex 含 Desktop、DSH 含桌面 GUI)在 Enterprise 经 %USERPROFILE%\\.lian-jing 稳定入口连项目专用地址,人终验通过。Claude 条目经 claude CLI 写入、以 claude mcp get 为准;DSH 由托管块全局行(headless)+ 会话级插件(GUI/web/acp 按会话工作目录)路由;接入写入幂等;接入包改写块外带未提交改动的托管块文件时给出混合文件预警;P7 实测 MCP 服务器说明仅 Claude/DSH 带入模型上下文、Codex 不带,引导提示保留在 skill。2026-10-03 P8 一键接入设计定稿(docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md):A1 不前置;P4 分 L0 静态 / L1 零模型 / L3 无头会话,P8 只依赖 L1(P4a);拆为 P8-0、P8a–P8g 与 P4-0、P4a–P4e(P8-0、P8a 已完成;P8a 把「已接入」统一为有项目 token),P8 进行中;进度按拆分后卡数重算。待做 P5、P6(基于 P8c 的 WriteSink)。"
|
||||
position: [3500, 950]
|
||||
|
||||
- id: architecture-evolution
|
||||
|
||||
@ -215,13 +215,15 @@ skill 只是五个接入面之一,兼容层覆盖全部五面。
|
||||
- 依赖顺序:P8-0 → P8a → P8b → P8c → P4a(另依赖 P4-0)→ P8d → P8e → P8g;P8f 可选、独立,P8e 端到端验收前完成。P8 关键路径不依赖 L3。
|
||||
- verification: "2026-10-03:manifest.yaml 经 PyYAML safe_load 解析通过(agent-client-parity progress 35、edges 95 条);manifest 全文「P4/P8 依赖」「依赖 architecture-evolution A1」「A1 的 CLI 投影」0 处;设计文档入库 docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md,§9 十一项推荐项已勾选。"
|
||||
|
||||
### 📋 P8a — 「已接入」单一判定
|
||||
- status: todo
|
||||
### ✅ P8a — 「已接入」单一判定
|
||||
- status: done
|
||||
- complexity: S
|
||||
- files: src-tauri/src/mcp_inject.rs, src-tauri/src/mcp_tools.rs
|
||||
- depends: P8-0
|
||||
- acceptance: "agent_verify: [cargo test mcp_inject] in-memory DB + 假 CredentialStore:有 token、不在任何组的项目进入 reinject_all_mcp 的目标集合;组成员仍在;token 已删的项目不在(锁内复查语义不变);新函数的连接由调用方传入。[cargo test mcp_tools] list_all_projects 的「已注入」与 DSH 白名单、根目录迁移调用同一个函数:删除 .claude/liangjing.json 不改变判定,删除 token 后判为未注入。"
|
||||
- 起因:「已接入」现有三套判定——reinject_all_mcp 按组成员、DSH 白名单与根目录迁移按 token、list_all_projects 按 .claude/liangjing.json 是否存在;ensure_standalone_group 只在项目不在任何组时才注入,已在组内的项目重跑接入包不会修复本机注册。
|
||||
- 实现(2026-10-03):mcp_inject 新增 is_project_injected(store, project_id)(有项目 MCP token 即已接入),DSH 白名单、on_project_root_changed、list_all_projects 改为调用它;reinject_all_mcp 的目标由「组成员」扩为「组成员 ∪ 有 token 的登记项目」(reinject_targets),锁内复查改为 is_reinject_target(仍是组成员或仍有 token),连接与凭据存储由调用方注入。list_all_projects 拆出 render_all_projects(conn, store),顺带去掉循环内的二次取池。行为变化:独立接入(不在任何组)的项目在端口变更后也会被重注入;list_all_projects 对只剩兼容文件、token 已删的项目改报「未注入」。ensure_standalone_group 的「已在组内不注入」由 P8d 编排本体解决,本卡不动。
|
||||
- verification: "2026-10-03:cargo test 286 passed / 0 failed / 5 ignored;新增 reinject_targets_follow_single_injected_judgment(组成员无 token 仍是目标、独立有 token 进入目标、删 token 后锁内复查为否且移出目标)与 list_all_projects_judges_injection_by_token_not_compat_file(只有 liangjing.json 无 token → 未注入;有 token 删兼容文件 → 已注入;删 token → 未注入)。变异:屏蔽 reinject_targets 的 token 分支、list_all_projects 改回看 liangjing.json,两条新测试均失败。契约同步 docs/ai-context/project-context.yaml「被管理项目身份上下文」。"
|
||||
|
||||
### 📋 P8b — 客户端适配器注册表 + 结构化注入结果 + A1a 适应度测试
|
||||
- status: todo
|
||||
|
||||
@ -26,7 +26,7 @@ contracts:
|
||||
- ".lian-jing/context.json"
|
||||
- ".lian-jing/capability-packs.json"
|
||||
- ".claude/liangjing.json"
|
||||
doc: "`.lian-jing/context.json` 是 project_id 的工具无关单一事实来源;`.claude/liangjing.json` 仅为兼容投影;`.lian-jing/capability-packs.json` 是炼境分发基线的版本描述。lian-jing skill 以同一模板双投影:`.agents/skills/lian-jing/`(Codex、DSH 读取)与 `.claude/skills/lian-jing/`(Claude Code 不读 .agents/skills),两份内容必须一致。context.json 可选字段 mcp_scope=global 声明控制面项目(如炼境自身):Claude 不写 local 条目、DSH 不入白名单,保留全局地址与跨项目工具。迁移必须保留未知字段;验证必须返回显式状态与 upgrade_hint;升级仅覆盖炼境托管文件,绝不覆盖项目定制或手工 MCP 配置,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。apply_onboarding_pack 的模板结果 TemplateResult 可带 warning(混合文件预警):写入前 AGENTS.md、docs/ai-context/git-workflow.md 或 CLAUDE.md 在 git 中已有托管块(ONBOARDING_MANAGED / BLUEPRINT_MANAGED)之外的未提交改动(未跟踪文件带块外内容也算;比较只忽略 CRLF/LF 与文末托管块连同其前空白——即炼境追加托管块带来的差异,块外空行增删与行尾空格均算改动),且本次确实改写了该文件时给出;只提示不阻断、块外内容不动,非 git 仓库或 git 不可用时静默。CLAUDE.md 只由蓝图规则同步改写,仅在预警时补一条结果。模板分发之后的步骤(lefthook 激活、buff)报错时,已改写的混合文件名并入错误信息返回。"
|
||||
doc: "`.lian-jing/context.json` 是 project_id 的工具无关单一事实来源;`.claude/liangjing.json` 仅为兼容投影;`.lian-jing/capability-packs.json` 是炼境分发基线的版本描述。lian-jing skill 以同一模板双投影:`.agents/skills/lian-jing/`(Codex、DSH 读取)与 `.claude/skills/lian-jing/`(Claude Code 不读 .agents/skills),两份内容必须一致。context.json 可选字段 mcp_scope=global 声明控制面项目(如炼境自身):Claude 不写 local 条目、DSH 不入白名单,保留全局地址与跨项目工具。迁移必须保留未知字段;验证必须返回显式状态与 upgrade_hint;升级仅覆盖炼境托管文件,绝不覆盖项目定制或手工 MCP 配置,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。apply_onboarding_pack 的模板结果 TemplateResult 可带 warning(混合文件预警):写入前 AGENTS.md、docs/ai-context/git-workflow.md 或 CLAUDE.md 在 git 中已有托管块(ONBOARDING_MANAGED / BLUEPRINT_MANAGED)之外的未提交改动(未跟踪文件带块外内容也算;比较只忽略 CRLF/LF 与文末托管块连同其前空白——即炼境追加托管块带来的差异,块外空行增删与行尾空格均算改动),且本次确实改写了该文件时给出;只提示不阻断、块外内容不动,非 git 仓库或 git 不可用时静默。CLAUDE.md 只由蓝图规则同步改写,仅在预警时补一条结果。模板分发之后的步骤(lefthook 激活、buff)报错时,已改写的混合文件名并入错误信息返回。「已接入」只有一个判定:本机受保护存储里有项目 MCP token(mcp_inject::is_project_injected);DSH 白名单、根目录迁移、批量重注入(产品组成员 + 有 token 的独立项目)与 list_all_projects 同源,.claude/liangjing.json 不作判据。"
|
||||
|
||||
- label: Tauri command 接口(前后端契约)
|
||||
paths:
|
||||
|
||||
@ -803,12 +803,7 @@ fn dsh_project_roots(
|
||||
let value = if context.get("mcp_scope").and_then(Value::as_str) == Some("global") {
|
||||
None
|
||||
} else {
|
||||
let has_token = crate::credential_store::mcp_auth_digest(&id)
|
||||
.and_then(|digest| {
|
||||
crate::credential_store::read_mcp_auth_token_for_digest(store, &digest)
|
||||
})
|
||||
.is_ok();
|
||||
if !has_token {
|
||||
if !is_project_injected(store, &id) {
|
||||
return None;
|
||||
}
|
||||
Some(id)
|
||||
@ -1412,45 +1407,86 @@ pub fn migrate_to_single_server() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 端口变更后重新注入所有产品组成员的 MCP 配置
|
||||
/// 「已接入」的单一判定:本机受保护存储里有该项目的 MCP 认证 token。
|
||||
/// token 由注入创建、由 remove_project_mcp 最先删除;DSH 白名单、根目录迁移、批量重注入与
|
||||
/// list_all_projects 都以此为准(P8a)。项目里的 .claude/liangjing.json 只是兼容投影,可能残留或缺失,不作判据。
|
||||
pub(crate) fn is_project_injected(
|
||||
store: &impl crate::credential_store::CredentialStore,
|
||||
project_id: &str,
|
||||
) -> bool {
|
||||
crate::credential_store::mcp_auth_digest(project_id)
|
||||
.and_then(|digest| crate::credential_store::read_mcp_auth_token_for_digest(store, &digest))
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// 批量重注入的目标:产品组成员(历史上入组即注入),加上已接入(有 token)但不在任何组的登记项目。
|
||||
/// 后者此前被漏掉——独立接入的项目在端口变更后不会被重注入,项目配置仍指向旧端口。
|
||||
fn reinject_targets(
|
||||
conn: &rusqlite::Connection,
|
||||
store: &impl crate::credential_store::CredentialStore,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let query_ids = |sql: &str| -> Result<Vec<String>, String> {
|
||||
let mut stmt = conn.prepare(sql).map_err(|e| e.to_string())?;
|
||||
let ids = stmt
|
||||
.query_map([], |row| row.get(0))
|
||||
.map_err(|e| e.to_string())?
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
Ok(ids)
|
||||
};
|
||||
let mut targets: std::collections::BTreeSet<String> =
|
||||
query_ids("SELECT DISTINCT project_id FROM project_group_map")?
|
||||
.into_iter()
|
||||
.collect();
|
||||
for id in query_ids("SELECT id FROM project_workspaces")? {
|
||||
if is_project_injected(store, &id) {
|
||||
targets.insert(id);
|
||||
}
|
||||
}
|
||||
Ok(targets.into_iter().collect())
|
||||
}
|
||||
|
||||
/// 锁内复查:目标快照取自锁外,期间被移出组且 token 已删的项目不再注入(避免重建刚删除的 token)。
|
||||
fn is_reinject_target(
|
||||
conn: &rusqlite::Connection,
|
||||
store: &impl crate::credential_store::CredentialStore,
|
||||
project_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
let member = conn
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM project_group_map WHERE project_id = ?1)",
|
||||
[project_id],
|
||||
|row| row.get::<_, bool>(0),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(member || is_project_injected(store, project_id))
|
||||
}
|
||||
|
||||
/// 端口变更后重新注入所有已接入项目(产品组成员 + 有 token 的独立项目)的 MCP 配置
|
||||
/// 返回注入失败的错误列表(部分失败不影响其他项目)
|
||||
pub fn reinject_all_mcp() -> Vec<String> {
|
||||
let conn = match db::pool().get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return vec![e.to_string()],
|
||||
let store = crate::credential_store::WindowsCredentialStore;
|
||||
let project_ids = match db::pool()
|
||||
.get()
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|conn| reinject_targets(&conn, &store))
|
||||
{
|
||||
Ok(ids) => ids,
|
||||
Err(e) => return vec![e],
|
||||
};
|
||||
|
||||
let project_ids: Vec<String> = {
|
||||
let mut stmt = match conn.prepare("SELECT DISTINCT project_id FROM project_group_map") {
|
||||
Ok(s) => s,
|
||||
Err(e) => return vec![e.to_string()],
|
||||
};
|
||||
stmt.query_map([], |row| row.get(0))
|
||||
.map(|iter| iter.filter_map(|r| r.ok()).collect())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
drop(conn);
|
||||
|
||||
let mut errors = Vec::new();
|
||||
for project_id in project_ids {
|
||||
let _guard = mcp_op_lock();
|
||||
// 快照取自锁外:锁内复查成员关系,已被移出组的项目不再重新注入(避免重建刚删除的 token)
|
||||
let still_member = db::pool()
|
||||
let still_target = db::pool()
|
||||
.get()
|
||||
.map_err(|error| error.to_string())
|
||||
.and_then(|conn| {
|
||||
conn.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM project_group_map WHERE project_id = ?1)",
|
||||
[&project_id],
|
||||
|row| row.get::<_, bool>(0),
|
||||
)
|
||||
.map_err(|error| error.to_string())
|
||||
});
|
||||
match still_member {
|
||||
.and_then(|conn| is_reinject_target(&conn, &store, &project_id));
|
||||
match still_target {
|
||||
Ok(true) => {}
|
||||
Ok(false) => continue,
|
||||
Err(error) => {
|
||||
errors.push(format!("项目 {project_id}: 复查组成员关系失败: {error}"));
|
||||
errors.push(format!("项目 {project_id}: 复查接入状态失败: {error}"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -1473,15 +1509,7 @@ pub fn on_project_root_changed(project_id: &str, old_root: Option<PathBuf>) {
|
||||
if let Some(root) = old_root {
|
||||
sync_claude_local(project_id, &root, None);
|
||||
}
|
||||
let injected = crate::credential_store::mcp_auth_digest(project_id)
|
||||
.and_then(|digest| {
|
||||
crate::credential_store::read_mcp_auth_token_for_digest(
|
||||
&crate::credential_store::WindowsCredentialStore,
|
||||
&digest,
|
||||
)
|
||||
})
|
||||
.is_ok();
|
||||
if !injected {
|
||||
if !is_project_injected(&crate::credential_store::WindowsCredentialStore, project_id) {
|
||||
refresh_dsh_patch(None);
|
||||
return;
|
||||
}
|
||||
@ -1717,6 +1745,51 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reinject_targets_follow_single_injected_judgment() {
|
||||
let conn = crate::db::conn_with_schema();
|
||||
let store = crate::credential_store::MemoryCredentialStore::new();
|
||||
conn.execute(
|
||||
"INSERT INTO product_groups (id, name) VALUES ('g1', 'G')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
for id in ["grouped", "standalone", "removed"] {
|
||||
conn.execute(
|
||||
"INSERT INTO project_profiles (id, name) VALUES (?1, 'QA')",
|
||||
[format!("{id}-profile")],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO project_workspaces (id, profile_id, win_path) VALUES (?1, ?2, ?3)",
|
||||
rusqlite::params![id, format!("{id}-profile"), format!("C:\\qa\\{id}")],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
conn.execute(
|
||||
"INSERT INTO project_group_map (project_id, group_id) VALUES ('grouped', 'g1')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
// standalone:独立接入(有 token、不在任何组),此前被批量重注入漏掉;removed:从未接入或已移除
|
||||
crate::credential_store::get_or_create_mcp_auth_token(&store, "standalone").unwrap();
|
||||
assert!(is_project_injected(&store, "standalone"));
|
||||
assert!(!is_project_injected(&store, "removed"));
|
||||
assert_eq!(
|
||||
reinject_targets(&conn, &store).unwrap(),
|
||||
vec!["grouped".to_string(), "standalone".to_string()]
|
||||
);
|
||||
// 锁内复查:组成员仍是目标;独立项目的 token 先被 remove_project_mcp 删掉后不再注入
|
||||
assert!(is_reinject_target(&conn, &store, "grouped").unwrap());
|
||||
assert!(is_reinject_target(&conn, &store, "standalone").unwrap());
|
||||
crate::credential_store::delete_mcp_auth_token_for_project(&store, "standalone").unwrap();
|
||||
assert!(!is_reinject_target(&conn, &store, "standalone").unwrap());
|
||||
assert_eq!(
|
||||
reinject_targets(&conn, &store).unwrap(),
|
||||
vec!["grouped".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_scope_projects_are_excluded_and_roots_follow_registry() {
|
||||
let base = std::env::temp_dir().join(format!("lianjing_dsh_roots_{}", std::process::id()));
|
||||
|
||||
@ -1997,6 +1997,22 @@ fn get_project_snapshots(project_id: &str, days: u32) -> Result<String, String>
|
||||
|
||||
pub fn list_all_projects() -> Result<String, String> {
|
||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||||
render_all_projects(&conn, &crate::credential_store::WindowsCredentialStore)
|
||||
}
|
||||
|
||||
/// 「已注入」按 mcp_inject::is_project_injected(有项目 token)判定,与 DSH 白名单、根目录迁移、批量重注入同源;
|
||||
/// 不再看项目里的 .claude/liangjing.json(兼容投影,可能残留或缺失)。
|
||||
fn render_all_projects(
|
||||
conn: &rusqlite::Connection,
|
||||
store: &impl crate::credential_store::CredentialStore,
|
||||
) -> Result<String, String> {
|
||||
let wsl_distro = conn
|
||||
.query_row(
|
||||
"SELECT value FROM settings WHERE key = 'wsl_distro'",
|
||||
[],
|
||||
|r| r.get::<_, String>(0),
|
||||
)
|
||||
.unwrap_or_else(|_| "Ubuntu".to_string());
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT pw.id, pp.name, pw.platform, pw.win_path, pw.wsl_path, pgm.group_id
|
||||
@ -2037,35 +2053,14 @@ pub fn list_all_projects() -> Result<String, String> {
|
||||
for (id, name, platform, win_path, wsl_path, group_id) in &rows {
|
||||
let project_path = if platform.as_deref() == Some("wsl") {
|
||||
wsl_path.as_ref().map(|p| {
|
||||
let conn2 = db::pool().get().ok();
|
||||
let distro = conn2
|
||||
.and_then(|c| {
|
||||
c.query_row(
|
||||
"SELECT value FROM settings WHERE key = 'wsl_distro'",
|
||||
[],
|
||||
|r| r.get::<_, String>(0),
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_else(|| "Ubuntu".to_string());
|
||||
let inner = p.trim_start_matches('/').replace('/', "\\");
|
||||
format!("\\\\wsl.localhost\\{}\\{}", distro, inner)
|
||||
format!("\\\\wsl.localhost\\{}\\{}", wsl_distro, inner)
|
||||
})
|
||||
} else {
|
||||
win_path.clone()
|
||||
};
|
||||
|
||||
let injected = project_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
std::path::Path::new(p)
|
||||
.join(".claude")
|
||||
.join("liangjing.json")
|
||||
.exists()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let inject_icon = if injected {
|
||||
let inject_icon = if crate::mcp_inject::is_project_injected(store, id) {
|
||||
"✅ 已注入"
|
||||
} else {
|
||||
"⚠️ 未注入"
|
||||
@ -2203,6 +2198,44 @@ pub fn scan_unregistered_projects() -> Result<String, String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn list_all_projects_judges_injection_by_token_not_compat_file() {
|
||||
let conn = crate::db::conn_with_schema();
|
||||
let store = crate::credential_store::MemoryCredentialStore::new();
|
||||
let root = std::env::temp_dir().join(format!("lj_list_inject_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(root.join(".claude")).unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO project_profiles (id, name) VALUES ('p-profile', 'QA')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO project_workspaces (id, profile_id, win_path) VALUES ('p', 'p-profile', ?1)",
|
||||
[root.display().to_string()],
|
||||
)
|
||||
.unwrap();
|
||||
let injected = |out: &str| {
|
||||
assert_ne!(
|
||||
out.contains("✅ 已注入"),
|
||||
out.contains("⚠️ 未注入"),
|
||||
"单个项目只能有一种状态"
|
||||
);
|
||||
out.contains("✅ 已注入")
|
||||
};
|
||||
// 只有兼容投影 .claude/liangjing.json、没有 token:未注入
|
||||
std::fs::write(root.join(".claude/liangjing.json"), "{}").unwrap();
|
||||
assert!(!injected(&render_all_projects(&conn, &store).unwrap()));
|
||||
// 有 token、删掉兼容文件:已注入(与 DSH 白名单、根目录迁移同一判定)
|
||||
crate::credential_store::get_or_create_mcp_auth_token(&store, "p").unwrap();
|
||||
std::fs::remove_file(root.join(".claude/liangjing.json")).unwrap();
|
||||
assert!(injected(&render_all_projects(&conn, &store).unwrap()));
|
||||
// 删除 token:未注入
|
||||
crate::credential_store::delete_mcp_auth_token_for_project(&store, "p").unwrap();
|
||||
assert!(!injected(&render_all_projects(&conn, &store).unwrap()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tools_list_contains_expected_tools() {
|
||||
let result = tools_list_result();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user