- 实现 DocOS 核心逻辑 (docos.rs),支持文档树管理 - 实现裂变 (Fission) 能力: 自动将文件提升为目录结构 - 实现聚合 (Fusion) 能力: 将目录结构降级为单文件,保持内容完整 - 实现统一 Outline 能力: 无论物理存储是文件还是目录,均提供一致的树状大纲(支持 Markdown Header 解析) - 新增相关单元测试 (docos_tests.rs) - 更新 types.rs 支持 DocNodeKind::Section - 引入 regex 依赖用于标题解析
142 lines
4.9 KiB
Rust
142 lines
4.9 KiB
Rust
use workflow_context::{ContextStore, Vgcs, DocOS, DocManager, DocNodeKind};
|
|
use tempfile::TempDir;
|
|
use std::sync::Arc;
|
|
|
|
const ZERO_OID: &str = "0000000000000000000000000000000000000000";
|
|
|
|
#[test]
|
|
fn test_docos_basic() -> anyhow::Result<()> {
|
|
let temp_dir = TempDir::new()?;
|
|
let store = Arc::new(Vgcs::new(temp_dir.path()));
|
|
let req_id = "req-docos-1";
|
|
|
|
store.init_repo(req_id)?;
|
|
|
|
// 1. Init DocOS with empty repo
|
|
let mut docos = DocOS::new(store.clone(), req_id, ZERO_OID);
|
|
|
|
// 2. Create a file (Leaf)
|
|
docos.write_content("Introduction", "Intro Content")?;
|
|
let _commit_1 = docos.save("Add Intro")?;
|
|
|
|
// 3. Verify outline
|
|
let outline = docos.get_outline()?;
|
|
// Root -> [Introduction (Leaf)]
|
|
assert_eq!(outline.children.len(), 1);
|
|
let intro_node = &outline.children[0];
|
|
assert_eq!(intro_node.name, "Introduction");
|
|
assert_eq!(intro_node.kind, DocNodeKind::Leaf);
|
|
|
|
// 4. Read content
|
|
let content = docos.read_content("Introduction")?;
|
|
assert_eq!(content, "Intro Content");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_docos_fission() -> anyhow::Result<()> {
|
|
let temp_dir = TempDir::new()?;
|
|
let store = Arc::new(Vgcs::new(temp_dir.path()));
|
|
let req_id = "req-docos-2";
|
|
store.init_repo(req_id)?;
|
|
|
|
let mut docos = DocOS::new(store.clone(), req_id, ZERO_OID);
|
|
|
|
// 1. Start with a Leaf: "Analysis"
|
|
docos.write_content("Analysis", "General Analysis")?;
|
|
let commit_1 = docos.save("Init Analysis")?;
|
|
|
|
// 2. Insert subsection "Revenue" into "Analysis"
|
|
// This should promote "Analysis" to Composite
|
|
docos.reload(&commit_1)?;
|
|
docos.insert_subsection("Analysis", "Revenue", "Revenue Data")?;
|
|
let commit_2 = docos.save("Split Analysis")?;
|
|
|
|
// 3. Verify Structure
|
|
docos.reload(&commit_2)?;
|
|
let outline = docos.get_outline()?;
|
|
|
|
// Root -> [Analysis (Composite)]
|
|
assert_eq!(outline.children.len(), 1);
|
|
let analysis_node = &outline.children[0];
|
|
assert_eq!(analysis_node.name, "Analysis");
|
|
assert_eq!(analysis_node.kind, DocNodeKind::Composite);
|
|
|
|
// Analysis -> [Revenue (Leaf)] (index.md is hidden in outline)
|
|
assert_eq!(analysis_node.children.len(), 1);
|
|
let revenue_node = &analysis_node.children[0];
|
|
assert_eq!(revenue_node.name, "Revenue");
|
|
assert_eq!(revenue_node.kind, DocNodeKind::Leaf);
|
|
|
|
// 4. Verify Content
|
|
// Reading "Analysis" should now read "Analysis/index.md" which contains "General Analysis"
|
|
let analysis_content = docos.read_content("Analysis")?;
|
|
assert_eq!(analysis_content, "General Analysis");
|
|
|
|
let revenue_content = docos.read_content("Analysis/Revenue")?;
|
|
assert_eq!(revenue_content, "Revenue Data");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_docos_fusion_and_outline() -> anyhow::Result<()> {
|
|
let temp_dir = TempDir::new()?;
|
|
let store = Arc::new(Vgcs::new(temp_dir.path()));
|
|
let req_id = "req-docos-3";
|
|
store.init_repo(req_id)?;
|
|
|
|
let mut docos = DocOS::new(store.clone(), req_id, ZERO_OID);
|
|
|
|
// 1. Create a composite structure (Pre-fissioned state)
|
|
// Root -> [Chapter1 (Composite)] -> [SectionA (Leaf), SectionB (Leaf)]
|
|
docos.write_content("Chapter1/index.md", "Chapter 1 Intro")?;
|
|
docos.write_content("Chapter1/SectionA", "Content A")?;
|
|
docos.write_content("Chapter1/SectionB", "Content B")?;
|
|
let commit_1 = docos.save("Setup Structure")?;
|
|
|
|
docos.reload(&commit_1)?;
|
|
|
|
// Verify Initial Outline
|
|
let outline_1 = docos.get_outline()?;
|
|
let ch1 = &outline_1.children[0];
|
|
assert_eq!(ch1.kind, DocNodeKind::Composite);
|
|
assert_eq!(ch1.children.len(), 2); // SectionA, SectionB
|
|
|
|
// 2. Demote (Fusion)
|
|
docos.demote("Chapter1")?;
|
|
let commit_2 = docos.save("Demote Chapter 1")?;
|
|
|
|
// 3. Verify Fusion Result
|
|
docos.reload(&commit_2)?;
|
|
let outline_2 = docos.get_outline()?;
|
|
|
|
// Now Chapter1 should be a Leaf
|
|
let ch1_fused = &outline_2.children[0];
|
|
assert_eq!(ch1_fused.name, "Chapter1");
|
|
assert_eq!(ch1_fused.kind, DocNodeKind::Leaf);
|
|
|
|
// But wait! Because of our Outline Enhancement (Markdown Headers),
|
|
// we expect the Fused file to have children (Sections) derived from headers!
|
|
// The demote logic appends children with "# Name".
|
|
// So "SectionA" became "# SectionA".
|
|
|
|
// Let's inspect the children of the Fused node
|
|
// We expect 2 children: "SectionA" and "SectionB" (as Sections)
|
|
assert_eq!(ch1_fused.children.len(), 2);
|
|
assert_eq!(ch1_fused.children[0].name, "SectionA");
|
|
assert_eq!(ch1_fused.children[0].kind, DocNodeKind::Section);
|
|
assert_eq!(ch1_fused.children[1].name, "SectionB");
|
|
|
|
// 4. Verify Content of Fused File
|
|
let content = docos.read_content("Chapter1")?;
|
|
// Should contain Intro + # SectionA ... + # SectionB ...
|
|
assert!(content.contains("Chapter 1 Intro"));
|
|
assert!(content.contains("# SectionA"));
|
|
assert!(content.contains("Content A"));
|
|
assert!(content.contains("# SectionB"));
|
|
|
|
Ok(())
|
|
}
|