f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
612 lines
21 KiB
Rust
612 lines
21 KiB
Rust
//! Black-box integration tests for skill & rule management (test-plan SM, RM, CP).
|
|
//!
|
|
//! These tests exercise the public API surface of `nomifun_extension::skill_service`
|
|
//! and `nomifun_extension::external_paths` against the functional requirements in
|
|
//! `08-file-workspace.md` §B.
|
|
|
|
use std::path::Path;
|
|
|
|
use nomifun_extension::external_paths::ExternalPathsManager;
|
|
use nomifun_extension::skill_service::{
|
|
NamedPath, SkillPaths, delete_assistant_rule, delete_assistant_skill, delete_skill,
|
|
detect_and_count_external_skills, export_skill_with_symlink, import_skill, import_skill_with_symlink,
|
|
list_available_skills, read_assistant_rule, read_assistant_skill, read_builtin_rule, read_builtin_skill,
|
|
read_skill_info, resolve_skill_paths, scan_for_skills, write_assistant_rule, write_assistant_skill,
|
|
};
|
|
use tempfile::TempDir;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const SKILL_MD: &str = "SKILL.md";
|
|
|
|
fn make_paths(base: &Path) -> SkillPaths {
|
|
SkillPaths {
|
|
data_dir: base.to_path_buf(),
|
|
user_skills_dir: base.join("skills"),
|
|
cron_skills_dir: base.join("cron").join("skills"),
|
|
builtin_skills_dir: base.join("builtin-skills"),
|
|
builtin_rules_dir: base.join("builtin-rules"),
|
|
assistant_rules_dir: base.join("assistant-rules"),
|
|
assistant_skills_dir: base.join("assistant-skills"),
|
|
}
|
|
}
|
|
|
|
fn builtin_dir(paths: &SkillPaths) -> &Path {
|
|
&paths.builtin_skills_dir
|
|
}
|
|
|
|
fn create_skill(base: &Path, name: &str, desc: &str) {
|
|
let dir = base.join(name);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
std::fs::write(
|
|
dir.join(SKILL_MD),
|
|
format!("---\nname: {name}\ndescription: {desc}\n---\nBody of {name}."),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn create_builtin_rule(base: &Path, name: &str, content: &str) {
|
|
std::fs::create_dir_all(base).unwrap();
|
|
std::fs::write(base.join(name), content).unwrap();
|
|
}
|
|
|
|
fn create_builtin_skill(base: &Path, name: &str, content: &str) {
|
|
std::fs::create_dir_all(base).unwrap();
|
|
std::fs::write(base.join(name), content).unwrap();
|
|
}
|
|
|
|
// ===========================================================================
|
|
// SM — Skill Management
|
|
// ===========================================================================
|
|
|
|
/// SM-1: List available skills (builtin + custom, deduplication).
|
|
#[tokio::test]
|
|
async fn sm1_list_available_skills_deduplication() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
// 3 built-in, 2 custom (1 overlaps)
|
|
create_skill(builtin_dir(&paths), "review", "Built-in review");
|
|
create_skill(builtin_dir(&paths), "debug", "Built-in debug");
|
|
create_skill(builtin_dir(&paths), "test", "Built-in test");
|
|
create_skill(&paths.user_skills_dir, "review", "Custom review override");
|
|
create_skill(&paths.user_skills_dir, "my-tool", "My custom tool");
|
|
|
|
let skills = list_available_skills(&paths).await.unwrap();
|
|
|
|
// 4 total: debug(builtin) + my-tool(custom) + review(custom) + test(builtin)
|
|
assert_eq!(skills.len(), 4);
|
|
|
|
// Verify deduplication: review should be the custom version
|
|
let review = skills.iter().find(|s| s.name == "review").unwrap();
|
|
assert!(review.is_custom);
|
|
assert_eq!(review.description, "Custom review override");
|
|
|
|
// Verify each has required fields
|
|
for s in &skills {
|
|
assert!(!s.name.is_empty());
|
|
assert!(!s.description.is_empty());
|
|
assert!(!s.location.is_empty());
|
|
}
|
|
}
|
|
|
|
/// SM-2: Read skill info from path.
|
|
#[tokio::test]
|
|
async fn sm2_read_skill_info() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let skill_dir = tmp.path().join("my-skill");
|
|
std::fs::create_dir_all(&skill_dir).unwrap();
|
|
std::fs::write(
|
|
skill_dir.join(SKILL_MD),
|
|
"---\nname: my-skill\ndescription: A test skill\n---\nBody content.",
|
|
)
|
|
.unwrap();
|
|
|
|
let (name, desc) = read_skill_info(&skill_dir).await.unwrap();
|
|
assert_eq!(name, "my-skill");
|
|
assert_eq!(desc, "A test skill");
|
|
}
|
|
|
|
/// SM-2 error: Path does not exist → error.
|
|
#[tokio::test]
|
|
async fn sm2_read_skill_info_not_found() {
|
|
let result = read_skill_info(Path::new("/nonexistent/skill")).await;
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
/// SM-3: Import skill (copy).
|
|
#[tokio::test]
|
|
async fn sm3_import_skill_copy() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
let source = tmp.path().join("external-skill");
|
|
std::fs::create_dir_all(&source).unwrap();
|
|
std::fs::write(
|
|
source.join(SKILL_MD),
|
|
"---\nname: ext-tool\ndescription: External tool\n---\nContent.",
|
|
)
|
|
.unwrap();
|
|
std::fs::write(source.join("helper.py"), "print('hello')").unwrap();
|
|
|
|
let name = import_skill(&paths, &source).await.unwrap();
|
|
assert_eq!(name, "ext-tool");
|
|
|
|
// Verify files were copied
|
|
let imported = paths.user_skills_dir.join("ext-tool");
|
|
assert!(imported.join(SKILL_MD).exists());
|
|
assert!(imported.join("helper.py").exists());
|
|
}
|
|
|
|
/// SM-4: Import skill (symlink).
|
|
#[tokio::test]
|
|
async fn sm4_import_skill_symlink() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
let source = tmp.path().join("linked-skill");
|
|
std::fs::create_dir_all(&source).unwrap();
|
|
std::fs::write(
|
|
source.join(SKILL_MD),
|
|
"---\nname: linked\ndescription: Linked skill\n---\nBody.",
|
|
)
|
|
.unwrap();
|
|
|
|
let name = import_skill_with_symlink(&paths, &source).await.unwrap();
|
|
assert_eq!(name, "linked");
|
|
|
|
let link = paths.user_skills_dir.join("linked");
|
|
assert!(link.is_symlink());
|
|
|
|
// Verify content is accessible through the symlink
|
|
let content = std::fs::read_to_string(link.join(SKILL_MD)).unwrap();
|
|
assert!(content.contains("Linked skill"));
|
|
}
|
|
|
|
/// SM-5: Export skill (symlink).
|
|
#[tokio::test]
|
|
async fn sm5_export_skill_symlink() {
|
|
let tmp = TempDir::new().unwrap();
|
|
|
|
let source = tmp.path().join("source-skill");
|
|
std::fs::create_dir_all(&source).unwrap();
|
|
std::fs::write(
|
|
source.join(SKILL_MD),
|
|
"---\nname: source\ndescription: Source\n---\nBody.",
|
|
)
|
|
.unwrap();
|
|
|
|
let export_dir = tmp.path().join("exports");
|
|
export_skill_with_symlink(&source, &export_dir).await.unwrap();
|
|
|
|
let link = export_dir.join("source-skill");
|
|
assert!(link.is_symlink());
|
|
}
|
|
|
|
/// SM-6: Delete user custom skill.
|
|
#[tokio::test]
|
|
async fn sm6_delete_custom_skill() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
create_skill(&paths.user_skills_dir, "deletable", "Will be deleted");
|
|
assert!(paths.user_skills_dir.join("deletable").exists());
|
|
|
|
delete_skill(&paths, "deletable").await.unwrap();
|
|
assert!(!paths.user_skills_dir.join("deletable").exists());
|
|
}
|
|
|
|
/// SM-7: Delete built-in skill → rejected.
|
|
#[tokio::test]
|
|
async fn sm7_delete_builtin_skill_rejected() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
create_skill(builtin_dir(&paths), "protected", "Cannot delete");
|
|
|
|
let result = delete_skill(&paths, "protected").await;
|
|
assert!(result.is_err());
|
|
|
|
// Verify it still exists
|
|
assert!(builtin_dir(&paths).join("protected").exists());
|
|
}
|
|
|
|
/// SM-8: Scan for skills in a directory.
|
|
#[tokio::test]
|
|
async fn sm8_scan_for_skills() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let scan_dir = tmp.path().join("scan-target");
|
|
|
|
create_skill(&scan_dir, "alpha", "Alpha skill");
|
|
create_skill(&scan_dir, "beta", "Beta skill");
|
|
// Directory without SKILL.md
|
|
std::fs::create_dir_all(scan_dir.join("not-a-skill")).unwrap();
|
|
|
|
let skills = scan_for_skills(&scan_dir).await.unwrap();
|
|
assert_eq!(skills.len(), 2);
|
|
assert!(skills.iter().any(|s| s.name == "alpha"));
|
|
assert!(skills.iter().any(|s| s.name == "beta"));
|
|
}
|
|
|
|
/// SM-11: Get skill directory paths.
|
|
///
|
|
/// Production mode: no `NOMIFUN_BUILTIN_SKILLS_PATH` set — the built-in
|
|
/// skills tree lives at `{data_dir}/builtin-skills/`, populated at
|
|
/// startup by `startup_materialize::materialize_if_needed`. The user
|
|
/// skills directory is derived from `data_dir`, not `resource_dir`.
|
|
#[tokio::test]
|
|
async fn sm11_get_skill_paths() {
|
|
// Ensure the env var is unset for a deterministic assertion.
|
|
// Safe: the test runs single-threaded w.r.t. this env var.
|
|
// (SAFETY: `remove_var` is unsafe in 2024 edition due to process-wide
|
|
// side-effects.)
|
|
unsafe {
|
|
std::env::remove_var("NOMIFUN_BUILTIN_SKILLS_PATH");
|
|
}
|
|
|
|
let resource_dir = Path::new("/app/resources");
|
|
let data_dir = Path::new("/home/user/.nomifun");
|
|
let paths = resolve_skill_paths(resource_dir, data_dir);
|
|
|
|
assert!(paths.user_skills_dir.to_string_lossy().contains("skills"));
|
|
assert_eq!(
|
|
paths.builtin_skills_dir,
|
|
data_dir.join("builtin-skills"),
|
|
"production mode must resolve builtin_skills_dir under data_dir"
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// RM — Rule Management
|
|
// ===========================================================================
|
|
|
|
/// RM-1: Read built-in rule.
|
|
#[tokio::test]
|
|
async fn rm1_read_builtin_rule() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
create_builtin_rule(&paths.builtin_rules_dir, "code-review.md", "# Review Rules");
|
|
|
|
let content = read_builtin_rule(&paths, "code-review.md").await.unwrap();
|
|
assert_eq!(content, "# Review Rules");
|
|
}
|
|
|
|
/// RM-1 error: File not found → empty string.
|
|
#[tokio::test]
|
|
async fn rm1_read_builtin_rule_not_found() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
let content = read_builtin_rule(&paths, "nonexistent.md").await.unwrap();
|
|
assert!(content.is_empty());
|
|
}
|
|
|
|
/// RM-1 variant: Read built-in skill.
|
|
#[tokio::test]
|
|
async fn rm1_read_builtin_skill() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
create_builtin_skill(builtin_dir(&paths), "tdd.md", "# TDD Workflow");
|
|
|
|
let content = read_builtin_skill(&paths, "tdd.md").await.unwrap();
|
|
assert_eq!(content, "# TDD Workflow");
|
|
}
|
|
|
|
/// RM-2: Read assistant rule with locale fallback.
|
|
#[tokio::test]
|
|
async fn rm2_assistant_rule_locale_fallback() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
// Write both default and locale-specific
|
|
write_assistant_rule(&paths, "abc123", "Default rule", None)
|
|
.await
|
|
.unwrap();
|
|
write_assistant_rule(&paths, "abc123", "中文规则", Some("zh-CN"))
|
|
.await
|
|
.unwrap();
|
|
|
|
// 1. Matching locale → locale-specific content
|
|
let content = read_assistant_rule(&paths, "abc123", Some("zh-CN")).await.unwrap();
|
|
assert_eq!(content, "中文规则");
|
|
|
|
// 2. Non-matching locale → fallback to default
|
|
let content = read_assistant_rule(&paths, "abc123", Some("en-US")).await.unwrap();
|
|
assert_eq!(content, "Default rule");
|
|
|
|
// 3. No locale → default
|
|
let content = read_assistant_rule(&paths, "abc123", None).await.unwrap();
|
|
assert_eq!(content, "Default rule");
|
|
|
|
// 4. Not found → empty string
|
|
let content = read_assistant_rule(&paths, "missing", None).await.unwrap();
|
|
assert!(content.is_empty());
|
|
}
|
|
|
|
/// RM-3: Write assistant rule.
|
|
#[tokio::test]
|
|
async fn rm3_write_assistant_rule() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
let result = write_assistant_rule(&paths, "abc123", "New rule content", Some("en-US"))
|
|
.await
|
|
.unwrap();
|
|
assert!(result);
|
|
|
|
// Verify file created
|
|
let file = paths.assistant_rules_dir.join("abc123.en-US.md");
|
|
assert!(file.exists());
|
|
let content = std::fs::read_to_string(file).unwrap();
|
|
assert_eq!(content, "New rule content");
|
|
}
|
|
|
|
/// RM-4: Delete assistant rule (all locales).
|
|
#[tokio::test]
|
|
async fn rm4_delete_assistant_rule_all_locales() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
write_assistant_rule(&paths, "abc123", "Default", None).await.unwrap();
|
|
write_assistant_rule(&paths, "abc123", "Chinese", Some("zh-CN"))
|
|
.await
|
|
.unwrap();
|
|
write_assistant_rule(&paths, "abc123", "English", Some("en-US"))
|
|
.await
|
|
.unwrap();
|
|
|
|
let deleted = delete_assistant_rule(&paths, "abc123").await.unwrap();
|
|
assert!(deleted);
|
|
|
|
// Verify all versions removed
|
|
let content = read_assistant_rule(&paths, "abc123", None).await.unwrap();
|
|
assert!(content.is_empty());
|
|
let content = read_assistant_rule(&paths, "abc123", Some("zh-CN")).await.unwrap();
|
|
assert!(content.is_empty());
|
|
let content = read_assistant_rule(&paths, "abc123", Some("en-US")).await.unwrap();
|
|
assert!(content.is_empty());
|
|
}
|
|
|
|
/// RM-5: Read assistant skill with locale fallback (same as RM-2 pattern).
|
|
#[tokio::test]
|
|
async fn rm5_assistant_skill_locale_fallback() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
write_assistant_skill(&paths, "abc123", "Default skill", None)
|
|
.await
|
|
.unwrap();
|
|
write_assistant_skill(&paths, "abc123", "English skill", Some("en-US"))
|
|
.await
|
|
.unwrap();
|
|
|
|
let content = read_assistant_skill(&paths, "abc123", Some("en-US")).await.unwrap();
|
|
assert_eq!(content, "English skill");
|
|
|
|
let content = read_assistant_skill(&paths, "abc123", Some("fr-FR")).await.unwrap();
|
|
assert_eq!(content, "Default skill");
|
|
}
|
|
|
|
/// RM-6: Write and delete assistant skill.
|
|
#[tokio::test]
|
|
async fn rm6_write_and_delete_assistant_skill() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
write_assistant_skill(&paths, "abc123", "Content", None).await.unwrap();
|
|
write_assistant_skill(&paths, "abc123", "Locale", Some("zh-CN"))
|
|
.await
|
|
.unwrap();
|
|
|
|
let deleted = delete_assistant_skill(&paths, "abc123").await.unwrap();
|
|
assert!(deleted);
|
|
|
|
let content = read_assistant_skill(&paths, "abc123", None).await.unwrap();
|
|
assert!(content.is_empty());
|
|
}
|
|
|
|
// ===========================================================================
|
|
// CP — Custom External Paths
|
|
// ===========================================================================
|
|
|
|
/// CP-1: Get custom paths (initially empty).
|
|
#[tokio::test]
|
|
async fn cp1_get_custom_paths_empty() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let mgr = ExternalPathsManager::new(tmp.path()).await;
|
|
|
|
let paths = mgr.get_custom_external_paths().await;
|
|
assert!(paths.is_empty());
|
|
}
|
|
|
|
/// CP-2: Add custom path and verify persistence.
|
|
#[tokio::test]
|
|
async fn cp2_add_custom_path() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let mgr = ExternalPathsManager::new(tmp.path()).await;
|
|
|
|
mgr.add_custom_external_path("My Skills", "/home/user/skills")
|
|
.await
|
|
.unwrap();
|
|
|
|
let paths = mgr.get_custom_external_paths().await;
|
|
assert_eq!(paths.len(), 1);
|
|
assert_eq!(paths[0].name, "My Skills");
|
|
assert_eq!(paths[0].path, "/home/user/skills");
|
|
|
|
// Verify persistence across reload
|
|
drop(mgr);
|
|
let mgr2 = ExternalPathsManager::new(tmp.path()).await;
|
|
let paths = mgr2.get_custom_external_paths().await;
|
|
assert_eq!(paths.len(), 1);
|
|
}
|
|
|
|
/// CP-3: Remove custom path.
|
|
#[tokio::test]
|
|
async fn cp3_remove_custom_path() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let mgr = ExternalPathsManager::new(tmp.path()).await;
|
|
|
|
mgr.add_custom_external_path("A", "/path/a").await.unwrap();
|
|
mgr.add_custom_external_path("B", "/path/b").await.unwrap();
|
|
|
|
mgr.remove_custom_external_path("/path/a").await.unwrap();
|
|
|
|
let paths = mgr.get_custom_external_paths().await;
|
|
assert_eq!(paths.len(), 1);
|
|
assert_eq!(paths[0].path, "/path/b");
|
|
}
|
|
|
|
/// CP-4: Enable skills market.
|
|
#[tokio::test]
|
|
async fn cp4_enable_skills_market() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let mgr = ExternalPathsManager::new(tmp.path()).await;
|
|
|
|
mgr.enable_skills_market().await.unwrap();
|
|
|
|
let paths = mgr.get_custom_external_paths().await;
|
|
assert_eq!(paths.len(), 1);
|
|
assert_eq!(paths[0].name, "nomifun-skills");
|
|
}
|
|
|
|
/// CP-5: Disable skills market.
|
|
#[tokio::test]
|
|
async fn cp5_disable_skills_market() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let mgr = ExternalPathsManager::new(tmp.path()).await;
|
|
|
|
mgr.enable_skills_market().await.unwrap();
|
|
mgr.disable_skills_market().await.unwrap();
|
|
|
|
let paths = mgr.get_custom_external_paths().await;
|
|
assert!(paths.is_empty());
|
|
}
|
|
|
|
// ===========================================================================
|
|
// External skill discovery
|
|
// ===========================================================================
|
|
|
|
/// Test detect_and_count_external_skills with custom paths.
|
|
#[tokio::test]
|
|
async fn detect_external_skills_from_custom_paths() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let ext_dir = tmp.path().join("external-skills");
|
|
create_skill(&ext_dir, "ext-a", "External A");
|
|
create_skill(&ext_dir, "ext-b", "External B");
|
|
|
|
let custom_paths = vec![NamedPath {
|
|
name: "External".to_string(),
|
|
path: ext_dir.to_string_lossy().into_owned(),
|
|
}];
|
|
|
|
let sources = detect_and_count_external_skills(&custom_paths).await;
|
|
|
|
// Should have at least the custom path source
|
|
let external = sources
|
|
.iter()
|
|
.find(|s| s.name == "External")
|
|
.expect("custom external source should be found");
|
|
assert_eq!(external.skill_count, 2);
|
|
assert!(external.skills.iter().any(|s| s.name == "ext-a"));
|
|
assert!(external.skills.iter().any(|s| s.name == "ext-b"));
|
|
// `source` for custom paths is `custom-<abs-path>` — used by the renderer
|
|
// as a React key / testid suffix, and asserted by e2e spec
|
|
// `edge-cases.e2e.ts` (prefix `external-source-tab-custom-`).
|
|
assert_eq!(external.source, format!("custom-{}", ext_dir.to_string_lossy()));
|
|
assert!(external.source.starts_with("custom-"));
|
|
}
|
|
|
|
/// Custom paths with distinct filesystem locations get distinct slugs so
|
|
/// the renderer can use them as unique React keys / testid suffixes.
|
|
#[tokio::test]
|
|
async fn detect_external_skills_custom_sources_are_unique() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let dir_a = tmp.path().join("a");
|
|
let dir_b = tmp.path().join("b");
|
|
create_skill(&dir_a, "a-skill", "A");
|
|
create_skill(&dir_b, "b-skill", "B");
|
|
|
|
let custom_paths = vec![
|
|
NamedPath {
|
|
name: "A".into(),
|
|
path: dir_a.to_string_lossy().into_owned(),
|
|
},
|
|
NamedPath {
|
|
name: "B".into(),
|
|
path: dir_b.to_string_lossy().into_owned(),
|
|
},
|
|
];
|
|
|
|
let sources = detect_and_count_external_skills(&custom_paths).await;
|
|
let slugs: Vec<&str> = sources
|
|
.iter()
|
|
.filter(|s| s.name == "A" || s.name == "B")
|
|
.map(|s| s.source.as_str())
|
|
.collect();
|
|
assert_eq!(slugs.len(), 2);
|
|
assert_ne!(slugs[0], slugs[1]);
|
|
}
|
|
|
|
/// Verify path traversal is blocked in skill deletion.
|
|
#[tokio::test]
|
|
async fn security_path_traversal_blocked() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
// Path traversal attempts
|
|
assert!(delete_skill(&paths, "../escape").await.is_err());
|
|
assert!(delete_skill(&paths, "foo/bar").await.is_err());
|
|
assert!(delete_skill(&paths, "foo\\bar").await.is_err());
|
|
}
|
|
|
|
/// Verify built-in resource reads block path traversal.
|
|
#[tokio::test]
|
|
async fn security_builtin_read_path_traversal() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
assert!(read_builtin_rule(&paths, "../secret.md").await.is_err());
|
|
assert!(read_builtin_skill(&paths, "../../etc/passwd").await.is_err());
|
|
assert!(read_builtin_rule(&paths, "").await.is_err());
|
|
}
|
|
|
|
/// Verify assistant CRUD functions block path traversal in assistant_id.
|
|
#[tokio::test]
|
|
async fn security_assistant_crud_path_traversal_id() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
// read
|
|
assert!(read_assistant_rule(&paths, "../escape", None).await.is_err());
|
|
assert!(read_assistant_skill(&paths, "foo/bar", None).await.is_err());
|
|
|
|
// write
|
|
assert!(write_assistant_rule(&paths, "../escape", "x", None).await.is_err());
|
|
assert!(write_assistant_skill(&paths, "foo\\bar", "x", None).await.is_err());
|
|
|
|
// delete
|
|
assert!(delete_assistant_rule(&paths, "../escape").await.is_err());
|
|
assert!(delete_assistant_skill(&paths, "a/b").await.is_err());
|
|
}
|
|
|
|
/// Verify assistant read/write functions block path traversal in locale.
|
|
#[tokio::test]
|
|
async fn security_assistant_crud_path_traversal_locale() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let paths = make_paths(tmp.path());
|
|
|
|
assert!(read_assistant_rule(&paths, "valid", Some("../bad")).await.is_err());
|
|
assert!(
|
|
write_assistant_rule(&paths, "valid", "x", Some("../../evil"))
|
|
.await
|
|
.is_err()
|
|
);
|
|
assert!(read_assistant_skill(&paths, "valid", Some("a/b")).await.is_err());
|
|
assert!(write_assistant_skill(&paths, "valid", "x", Some("a\\b")).await.is_err());
|
|
}
|