WIP: Batch 2 — backfill tests for fs, shell, and http/workflow

- io/fs.rs: 20 tests (path resolution, project open/close/get, known projects,
  model prefs, file read/write, list dir, validate path, scaffold)
- io/shell.rs: 4 new tests (allowlist, command execution, stdout capture, exit codes)
- http/workflow.rs: 8 tests (parse_test_status, to_test_case, to_review_story)

Coverage: 28.6% → 48.1%

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-19 13:52:19 +00:00
parent 76e7c68b66
commit de6334720a
3 changed files with 435 additions and 0 deletions

View File

@@ -696,8 +696,208 @@ pub async fn create_directory_absolute(path: String) -> Result<bool, String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::store::JsonFileStore;
use tempfile::tempdir;
fn make_store(dir: &tempfile::TempDir) -> JsonFileStore {
JsonFileStore::new(dir.path().join("test_store.json")).unwrap()
}
fn make_state_with_root(path: PathBuf) -> SessionState {
let state = SessionState::default();
{
let mut root = state.project_root.lock().unwrap();
*root = Some(path);
}
state
}
// --- resolve_path_impl ---
#[test]
fn resolve_path_joins_relative_to_root() {
let root = PathBuf::from("/projects/myapp");
let result = resolve_path_impl(root, "src/main.rs").unwrap();
assert_eq!(result, PathBuf::from("/projects/myapp/src/main.rs"));
}
#[test]
fn resolve_path_rejects_traversal() {
let root = PathBuf::from("/projects/myapp");
let result = resolve_path_impl(root, "../etc/passwd");
assert!(result.is_err());
assert!(result.unwrap_err().contains("traversal"));
}
// --- is_story_kit_path ---
#[test]
fn is_story_kit_path_matches_root_and_children() {
assert!(is_story_kit_path(".story_kit"));
assert!(is_story_kit_path(".story_kit/stories/current/26.md"));
assert!(!is_story_kit_path("src/main.rs"));
assert!(!is_story_kit_path(".story_kit_other"));
}
// --- open/close/get project ---
#[tokio::test]
async fn open_project_sets_root_and_persists() {
let dir = tempdir().unwrap();
let project_dir = dir.path().join("myproject");
fs::create_dir_all(&project_dir).unwrap();
let store = make_store(&dir);
let state = SessionState::default();
let result = open_project(
project_dir.to_string_lossy().to_string(),
&state,
&store,
)
.await;
assert!(result.is_ok());
let root = state.get_project_root().unwrap();
assert_eq!(root, project_dir);
}
#[tokio::test]
async fn close_project_clears_root() {
let dir = tempdir().unwrap();
let project_dir = dir.path().join("myproject");
fs::create_dir_all(&project_dir).unwrap();
let store = make_store(&dir);
let state = make_state_with_root(project_dir);
close_project(&state, &store).unwrap();
let root = state.project_root.lock().unwrap();
assert!(root.is_none());
}
#[tokio::test]
async fn get_current_project_returns_none_when_no_project() {
let dir = tempdir().unwrap();
let store = make_store(&dir);
let state = SessionState::default();
let result = get_current_project(&state, &store).unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn get_current_project_returns_active_root() {
let dir = tempdir().unwrap();
let store = make_store(&dir);
let state = make_state_with_root(dir.path().to_path_buf());
let result = get_current_project(&state, &store).unwrap();
assert!(result.is_some());
}
// --- known projects ---
#[test]
fn known_projects_empty_by_default() {
let dir = tempdir().unwrap();
let store = make_store(&dir);
let projects = get_known_projects(&store).unwrap();
assert!(projects.is_empty());
}
#[tokio::test]
async fn open_project_adds_to_known_projects() {
let dir = tempdir().unwrap();
let project_dir = dir.path().join("proj1");
fs::create_dir_all(&project_dir).unwrap();
let store = make_store(&dir);
let state = SessionState::default();
open_project(
project_dir.to_string_lossy().to_string(),
&state,
&store,
)
.await
.unwrap();
let projects = get_known_projects(&store).unwrap();
assert_eq!(projects.len(), 1);
}
#[test]
fn forget_known_project_removes_it() {
let dir = tempdir().unwrap();
let store = make_store(&dir);
store.set(KEY_KNOWN_PROJECTS, json!(["/a", "/b", "/c"]));
forget_known_project("/b".to_string(), &store).unwrap();
let projects = get_known_projects(&store).unwrap();
assert_eq!(projects, vec!["/a", "/c"]);
}
#[test]
fn forget_unknown_project_is_noop() {
let dir = tempdir().unwrap();
let store = make_store(&dir);
store.set(KEY_KNOWN_PROJECTS, json!(["/a"]));
forget_known_project("/nonexistent".to_string(), &store).unwrap();
let projects = get_known_projects(&store).unwrap();
assert_eq!(projects, vec!["/a"]);
}
// --- model preference ---
#[test]
fn model_preference_none_by_default() {
let dir = tempdir().unwrap();
let store = make_store(&dir);
assert!(get_model_preference(&store).unwrap().is_none());
}
#[test]
fn set_and_get_model_preference() {
let dir = tempdir().unwrap();
let store = make_store(&dir);
set_model_preference("claude-3-sonnet".to_string(), &store).unwrap();
assert_eq!(
get_model_preference(&store).unwrap(),
Some("claude-3-sonnet".to_string())
);
}
// --- file operations ---
#[tokio::test]
async fn read_file_impl_reads_content() {
let dir = tempdir().unwrap();
let file = dir.path().join("test.txt");
fs::write(&file, "hello world").unwrap();
let content = read_file_impl(file).await.unwrap();
assert_eq!(content, "hello world");
}
#[tokio::test]
async fn read_file_impl_errors_on_missing() {
let dir = tempdir().unwrap();
let result = read_file_impl(dir.path().join("missing.txt")).await;
assert!(result.is_err());
}
#[tokio::test]
async fn write_file_impl_creates_and_writes() {
let dir = tempdir().unwrap();
let file = dir.path().join("sub").join("output.txt");
write_file_impl(file.clone(), "content".to_string()).await.unwrap();
assert_eq!(fs::read_to_string(&file).unwrap(), "content");
}
#[tokio::test]
async fn write_file_requires_approved_test_plan() {
let dir = tempdir().expect("tempdir");
@@ -715,4 +915,74 @@ mod tests {
"expected write to be blocked when test plan is not approved"
);
}
// --- list directory ---
#[tokio::test]
async fn list_directory_impl_returns_sorted_entries() {
let dir = tempdir().unwrap();
fs::create_dir(dir.path().join("zdir")).unwrap();
fs::create_dir(dir.path().join("adir")).unwrap();
fs::write(dir.path().join("file.txt"), "").unwrap();
let entries = list_directory_impl(dir.path().to_path_buf()).await.unwrap();
assert_eq!(entries[0].name, "adir");
assert_eq!(entries[0].kind, "dir");
assert_eq!(entries[1].name, "zdir");
assert_eq!(entries[1].kind, "dir");
assert_eq!(entries[2].name, "file.txt");
assert_eq!(entries[2].kind, "file");
}
// --- validate_project_path ---
#[tokio::test]
async fn validate_project_path_rejects_missing() {
let result = validate_project_path(PathBuf::from("/nonexistent/path")).await;
assert!(result.is_err());
}
#[tokio::test]
async fn validate_project_path_rejects_file() {
let dir = tempdir().unwrap();
let file = dir.path().join("not_a_dir.txt");
fs::write(&file, "").unwrap();
let result = validate_project_path(file).await;
assert!(result.is_err());
}
#[tokio::test]
async fn validate_project_path_accepts_directory() {
let dir = tempdir().unwrap();
let result = validate_project_path(dir.path().to_path_buf()).await;
assert!(result.is_ok());
}
// --- scaffold ---
#[test]
fn scaffold_story_kit_creates_structure() {
let dir = tempdir().unwrap();
scaffold_story_kit(dir.path()).unwrap();
assert!(dir.path().join(".story_kit/README.md").exists());
assert!(dir.path().join(".story_kit/specs/README.md").exists());
assert!(dir.path().join(".story_kit/specs/00_CONTEXT.md").exists());
assert!(dir.path().join(".story_kit/specs/tech/STACK.md").exists());
assert!(dir.path().join(".story_kit/stories/archive").is_dir());
}
#[test]
fn scaffold_story_kit_does_not_overwrite_existing() {
let dir = tempdir().unwrap();
let readme = dir.path().join(".story_kit/README.md");
fs::create_dir_all(readme.parent().unwrap()).unwrap();
fs::write(&readme, "custom content").unwrap();
scaffold_story_kit(dir.path()).unwrap();
assert_eq!(fs::read_to_string(&readme).unwrap(), "custom content");
}
}

View File

@@ -106,4 +106,64 @@ mod tests {
"expected shell execution to be blocked when test plan is not approved"
);
}
#[tokio::test]
async fn exec_shell_impl_rejects_disallowed_command() {
let dir = tempdir().unwrap();
let result = exec_shell_impl(
"curl".to_string(),
vec!["https://example.com".to_string()],
dir.path().to_path_buf(),
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("not in the allowlist"));
}
#[tokio::test]
async fn exec_shell_impl_runs_allowed_command() {
let dir = tempdir().unwrap();
let result = exec_shell_impl(
"ls".to_string(),
Vec::new(),
dir.path().to_path_buf(),
)
.await;
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output.exit_code, 0);
}
#[tokio::test]
async fn exec_shell_impl_captures_stdout() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("hello.txt"), "").unwrap();
let result = exec_shell_impl(
"ls".to_string(),
Vec::new(),
dir.path().to_path_buf(),
)
.await
.unwrap();
assert!(result.stdout.contains("hello.txt"));
}
#[tokio::test]
async fn exec_shell_impl_returns_nonzero_exit_code() {
let dir = tempdir().unwrap();
let result = exec_shell_impl(
"ls".to_string(),
vec!["nonexistent_file_xyz".to_string()],
dir.path().to_path_buf(),
)
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(!result.stderr.is_empty());
}
}