- Implemented `ContextShell` trait providing `ls`, `find`, `grep`, `cat`, `patch`, `wc` primitives. - Added `WorkerContext` struct acting as the shell interface for LLM workers. - Implemented `get_tool_definitions` to export OpenAI-compatible tool schemas. - Added `patch` command for atomic, exact-match text replacement. - Added comprehensive tests covering happy paths and edge cases (ambiguity, unicode, regex errors). - Updated design documentation.
143 lines
5.1 KiB
Rust
143 lines
5.1 KiB
Rust
use workflow_context::{WorkerContext, ContextShell, OutputFormat, FindOptions, Vgcs, ContextStore};
|
|
use tempfile::TempDir;
|
|
|
|
const ZERO_OID: &str = "0000000000000000000000000000000000000000";
|
|
|
|
fn setup_env() -> (TempDir, String, String) {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let data_path = temp_dir.path().to_str().unwrap().to_string();
|
|
let req_id = "req-shell-test".to_string();
|
|
|
|
// Init Repo
|
|
let vgcs = Vgcs::new(&data_path);
|
|
vgcs.init_repo(&req_id).unwrap();
|
|
|
|
(temp_dir, data_path, req_id)
|
|
}
|
|
|
|
#[test]
|
|
fn test_shell_comprehensive() -> anyhow::Result<()> {
|
|
let (_tmp, data_path, req_id) = setup_env();
|
|
|
|
// 1. Setup Initial Context
|
|
let mut ctx = WorkerContext::new(&data_path, &req_id, ZERO_OID);
|
|
|
|
ctx.write_file("README.md", "Project Root\n\nIntroduction here.")?;
|
|
ctx.write_file("src/main.rs", "fn main() {\n println!(\"Hello\");\n println!(\"Hello\");\n}")?; // Double Hello for ambiguity test
|
|
ctx.write_file("src/util.rs", "pub fn util() -> i32 { 42 }")?;
|
|
ctx.write_file("data/config.json", "{\n \"key\": \"value\",\n \"retries\": 3\n}")?;
|
|
ctx.write_file("文档/说明.txt", "这是一个中文文件。")?; // Unicode Path & Content
|
|
|
|
let commit_1 = ctx.commit("Init")?;
|
|
let mut ctx = WorkerContext::new(&data_path, &req_id, &commit_1);
|
|
|
|
// --- Find Tests ---
|
|
println!("Testing Find...");
|
|
|
|
// Test: Recursive vs Non-recursive
|
|
// Note: Includes directories (src, data, 文档) + files (5) = 8
|
|
let all_nodes = ctx.find("**/*", FindOptions { recursive: true, ..Default::default() })?;
|
|
assert_eq!(all_nodes.len(), 8);
|
|
|
|
// Test: Only Files
|
|
let only_files = ctx.find("**/*", FindOptions {
|
|
recursive: true,
|
|
type_filter: Some("File".to_string()),
|
|
..Default::default()
|
|
})?;
|
|
assert_eq!(only_files.len(), 5);
|
|
|
|
// Test: Non-recursive (Top level)
|
|
let root_nodes = ctx.find("*", FindOptions { recursive: false, ..Default::default() })?;
|
|
// Expect README.md, src(dir), data(dir), 文档(dir)
|
|
assert!(root_nodes.iter().any(|f| f.path == "README.md"));
|
|
assert!(root_nodes.iter().any(|f| f.path == "src"));
|
|
|
|
// Test: Type Filter (Dir)
|
|
let dirs = ctx.find("**/*", FindOptions {
|
|
recursive: true,
|
|
type_filter: Some("Dir".to_string()),
|
|
..Default::default()
|
|
})?;
|
|
assert!(dirs.iter().any(|d| d.path == "src"));
|
|
assert!(dirs.iter().any(|d| d.path == "data"));
|
|
assert!(dirs.iter().any(|d| d.path == "文档"));
|
|
assert!(!dirs.iter().any(|d| d.path == "README.md"));
|
|
|
|
// --- Grep Tests ---
|
|
println!("Testing Grep...");
|
|
|
|
// Test: Regex Match
|
|
let matches = ctx.grep(r"fn \w+\(\)", None)?;
|
|
assert_eq!(matches.len(), 2); // main() and util()
|
|
|
|
// Test: Unicode Content
|
|
let zh_matches = ctx.grep("中文", None)?;
|
|
assert_eq!(zh_matches.len(), 1);
|
|
assert_eq!(zh_matches[0].path, "文档/说明.txt");
|
|
|
|
// Test: Invalid Regex
|
|
let bad_regex = ctx.grep("(", None);
|
|
assert!(bad_regex.is_err());
|
|
|
|
// --- Patch Tests ---
|
|
println!("Testing Patch...");
|
|
|
|
// Test: Ambiguous Match (Safety Check)
|
|
// src/main.rs has two "println!(\"Hello\");"
|
|
let res = ctx.patch("src/main.rs", "println!(\"Hello\");", "println!(\"World\");");
|
|
assert!(res.is_err(), "Should fail on ambiguous match");
|
|
let err_msg = res.unwrap_err().to_string();
|
|
assert!(err_msg.contains("Ambiguous match"), "Error message mismatch: {}", err_msg);
|
|
|
|
// Test: Unique Match
|
|
// Patch "Introduction here." to "Intro v2." in README.md
|
|
ctx.patch("README.md", "Introduction here.", "Intro v2.")?;
|
|
ctx.commit("Patch 1")?; // Must commit to verify via read (if read uses committed state)
|
|
|
|
// Verify
|
|
let readme = ctx.read_text("README.md")?;
|
|
assert!(readme.contains("Intro v2."));
|
|
|
|
// Test: Special Characters (Literal Match)
|
|
// Let's try to patch JSON which has braces and quotes
|
|
ctx.patch("data/config.json", "\"retries\": 3", "\"retries\": 5")?;
|
|
ctx.commit("Patch 2")?;
|
|
|
|
let config = ctx.read_text("data/config.json")?;
|
|
assert!(config.contains("\"retries\": 5"));
|
|
|
|
// Test: Cross-line Patch
|
|
// Replace the whole function body in util.rs
|
|
let old_block = "pub fn util() -> i32 { 42 }";
|
|
let new_block = "pub fn util() -> i32 {\n return 100;\n}";
|
|
ctx.patch("src/util.rs", old_block, new_block)?;
|
|
ctx.commit("Patch 3")?;
|
|
|
|
let util = ctx.read_text("src/util.rs")?;
|
|
assert!(util.contains("return 100;"));
|
|
|
|
// Test: Patch non-existent file
|
|
let res = ctx.patch("ghost.txt", "foo", "bar");
|
|
assert!(res.is_err());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_schema_validity() {
|
|
let defs = WorkerContext::get_tool_definitions();
|
|
assert!(defs.is_array());
|
|
let arr = defs.as_array().unwrap();
|
|
|
|
// Verify critical fields exist for OpenAI
|
|
for tool in arr {
|
|
let obj = tool.as_object().unwrap();
|
|
assert_eq!(obj["type"], "function");
|
|
let func = obj["function"].as_object().unwrap();
|
|
assert!(func.contains_key("name"));
|
|
assert!(func.contains_key("description"));
|
|
assert!(func.contains_key("parameters"));
|
|
}
|
|
}
|