#![cfg(test)]
use std::collections::HashMap;
use std::path::PathBuf;
use code_repo_wiki::config::schema::WikiConfig;
use code_repo_wiki::generate::card::CardGenerator;
use code_repo_wiki::generate::chunk::{chunk_by_file, Chunk};
use code_repo_wiki::generate::llm::{LlmProvider, Message, MockProvider};
use code_repo_wiki::ingest::parser::{Entity, FileInsight, ImportStmt};
fn make_test_chunk() -> Chunk {
let entity = Entity {
name: "Config".into(),
kind: "struct".into(),
line_start: 1,
line_end: 30,
doc_comment: Some("配置管理".into()),
signature: None,
visibility: None,
};
let insight = FileInsight {
path: PathBuf::from("src/config.rs"),
language: "rust".into(),
entities: vec![entity],
imports: vec![ImportStmt {
source: "serde".into(),
alias: None,
line: 1,
}],
doc_comments: vec![],
source: String::new(),
};
chunk_by_file(&insight)
}
fn make_empty_chunk(module: &str) -> Chunk {
let mut chunk = make_test_chunk();
chunk.module_path = vec![module.to_string()];
chunk.entities = Vec::new();
chunk.imports = Vec::new();
chunk
}
fn temp_config(tag: &str) -> (WikiConfig, std::path::PathBuf) {
let dir = std::env::temp_dir().join(format!(
"code_repo_wiki_it_v31_{tag}_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
(WikiConfig { output_dir: Some(dir.to_path_buf()), ..Default::default() }, dir)
}
#[tokio::test]
async fn all_empty_chunks_yield_empty_cards_and_no_failures() {
let (config, dir) = temp_config("all_empty");
let provider = MockProvider::new();
let generator = CardGenerator::new(&provider, config, 1, "zh".into());
let cards = generator
.generate_all_cards(&[make_empty_chunk("zzz_a"), make_empty_chunk("zzz_b")], &HashMap::new())
.await
.unwrap();
assert!(cards.is_empty(), "全空 chunk 应产出空卡片列表");
assert!(
generator.failed_modules().is_empty(),
"全空 chunk 不得记入 failed_modules: {:?}",
generator.failed_modules()
);
assert_eq!(generator.llm_call_count(), 0, "空 chunk 不得触发 LLM 调用");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn mixed_interleave_success_empty_failure_attributes_correctly() {
let mut other = make_test_chunk();
other.module_path = vec!["other".into()];
let selective = SelectiveFailingProvider { fail_on: "模块路径: other" };
let (config, dir) = temp_config("mixed");
let generator = CardGenerator::new(&selective, config, 1, "zh".into());
let cards = generator
.generate_all_cards(
&[make_test_chunk(), make_empty_chunk("zzz"), other],
&HashMap::new(),
)
.await
.unwrap();
assert_eq!(cards.len(), 1, "成功卡片保留(空 chunk 与失败模块不产出)");
assert_eq!(cards[0].module_name, "src", "成功卡片归因正确,不得被空 chunk 错位截断");
assert_eq!(
generator.failed_modules(),
vec!["other"],
"失败必须归因到真实失败模块(空 chunk 在中间不得错位): {:?}",
generator.failed_modules()
);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn empty_chunk_interleaved_with_failure_and_success() {
let failing = FailingProvider;
let (config1, dir1) = temp_config("empty_fail");
let gen1 = CardGenerator::new(&failing, config1, 1, "zh".into());
let cards1 = gen1
.generate_all_cards(&[make_empty_chunk("zzz"), make_test_chunk()], &HashMap::new())
.await
.unwrap();
assert!(cards1.is_empty(), "失败模块不产出卡片");
assert_eq!(
gen1.failed_modules(),
vec!["src"],
"失败必须归因到真实失败模块: {:?}",
gen1.failed_modules()
);
let provider = MockProvider::new();
let (config2, dir2) = temp_config("empty_ok");
let gen2 = CardGenerator::new(&provider, config2, 1, "zh".into());
let cards2 = gen2
.generate_all_cards(&[make_empty_chunk("zzz"), make_test_chunk()], &HashMap::new())
.await
.unwrap();
assert_eq!(cards2.len(), 1, "成功卡片不得静默丢失");
assert_eq!(cards2[0].module_name, "src");
assert!(
gen2.failed_modules().is_empty(),
"无真实失败时不记 failed_modules: {:?}",
gen2.failed_modules()
);
let _ = std::fs::remove_dir_all(&dir1);
let _ = std::fs::remove_dir_all(&dir2);
}
struct FailingProvider;
impl LlmProvider for FailingProvider {
async fn complete(&self, _messages: &[Message]) -> anyhow::Result<String> {
anyhow::bail!("模拟 LLM 调用失败")
}
async fn complete_stream(&self, _messages: &[Message]) -> anyhow::Result<Vec<String>> {
anyhow::bail!("模拟 LLM 调用失败")
}
fn call_count(&self) -> usize {
0
}
}
struct SelectiveFailingProvider {
fail_on: &'static str,
}
impl LlmProvider for SelectiveFailingProvider {
async fn complete(&self, messages: &[Message]) -> anyhow::Result<String> {
if messages.iter().any(|m| m.content.contains(self.fail_on)) {
anyhow::bail!("模拟 LLM 调用失败")
}
Ok(
r#"{"summary": "这是 Mock Provider 生成的模拟摘要", "key_entities": []}"#
.to_string(),
)
}
async fn complete_stream(&self, messages: &[Message]) -> anyhow::Result<Vec<String>> {
if messages.iter().any(|m| m.content.contains(self.fail_on)) {
anyhow::bail!("模拟 LLM 调用失败")
}
Ok(vec!["模拟流式响应 chunk".to_string()])
}
fn call_count(&self) -> usize {
0
}
}