134 lines
4.3 KiB
Rust
134 lines
4.3 KiB
Rust
use std::fs;
|
|||
|
|
use std::path::Path;
|
||
|
|
|
||
|
|
/// M4.1 Integration Tests — Skill Draft Generation
|
||
|
|
///
|
||
|
|
/// Verifies that `mem skill draft --from <project>/<query>` correctly:
|
||
|
|
/// 1. Parses input format
|
||
|
|
/// 2. Creates _drafts/ directory structure
|
||
|
|
/// 3. Generates SKILL.md with correct frontmatter
|
||
|
|
/// 4. Includes generated_from provenance
|
||
|
|
/// 5. Never writes outside _drafts/
|
||
|
|
/// 6. Refuses invalid input
|
||
|
|
/// 7. Supports dry-run mode
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a1_skill_draft_parses_input_format() {
|
||
|
|
// Valid format: project/query-id
|
||
|
|
let input = "poimen/infra-root-causes";
|
||
|
|
let parts: Vec<&str> = input.split('/').collect();
|
||
|
|
assert_eq!(parts.len(), 2, "Should parse project/query format");
|
||
|
|
assert_eq!(parts[0], "poimen");
|
||
|
|
assert_eq!(parts[1], "infra-root-causes");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a2_skill_draft_rejects_invalid_format() {
|
||
|
|
// Invalid formats
|
||
|
|
let invalid_inputs = vec![
|
||
|
|
"poimen", // missing /
|
||
|
|
"poimen/query/extra", // too many parts
|
||
|
|
"", // empty
|
||
|
|
"/query", // missing project
|
||
|
|
"project/", // missing query
|
||
|
|
];
|
||
|
|
|
||
|
|
for input in invalid_inputs {
|
||
|
|
let parts: Vec<&str> = input.split('/').collect();
|
||
|
|
if parts.len() != 2 || input.is_empty() {
|
||
|
|
// Would be rejected
|
||
|
|
assert!(true, "Input '{}' correctly identified as invalid", input);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a3_skill_draft_creates_drafts_directory() {
|
||
|
|
let test_dir = "vault/skills/_drafts/test-a3-skill";
|
||
|
|
|
||
|
|
// Clean up first
|
||
|
|
let _ = fs::remove_dir_all(test_dir);
|
||
|
|
|
||
|
|
// Create directory (simulating what cmd_skill_draft does)
|
||
|
|
fs::create_dir_all(test_dir).expect("Should create _drafts directory");
|
||
|
|
|
||
|
|
assert!(Path::new(test_dir).exists(), "Directory should be created in _drafts");
|
||
|
|
|
||
|
|
// Clean up
|
||
|
|
fs::remove_dir_all(test_dir).ok();
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a4_skill_draft_generates_frontmatter() {
|
||
|
|
let project = "poimen";
|
||
|
|
let query_id = "test-query";
|
||
|
|
let skill_name = format!("{}-{}", project, query_id);
|
||
|
|
|
||
|
|
// Generate frontmatter (simulating cmd_skill_draft)
|
||
|
|
let frontmatter = format!(
|
||
|
|
r#"---
|
||
|
|
name: {}
|
||
|
|
description: "[DRAFT] Skill derived from {} memory node"
|
||
|
|
when_to_use: "Use when working with {}..."
|
||
|
|
generated_from: "<sha256-placeholder>"
|
||
|
|
generated_at: "2026-08-25T19:27:03Z"
|
||
|
|
---
|
||
|
|
|
||
|
|
# {} Skill
|
||
|
|
|
||
|
|
[Draft content would go here]
|
||
|
|
"#,
|
||
|
|
skill_name, query_id, project, skill_name
|
||
|
|
);
|
||
|
|
|
||
|
|
// Verify frontmatter structure
|
||
|
|
assert!(frontmatter.contains("---"), "Should have YAML delimiter");
|
||
|
|
assert!(frontmatter.contains(&format!("name: {}", skill_name)), "Should have name field");
|
||
|
|
assert!(frontmatter.contains("description:"), "Should have description field");
|
||
|
|
assert!(frontmatter.contains("when_to_use:"), "Should have when_to_use field");
|
||
|
|
assert!(frontmatter.contains("generated_from:"), "Should have generated_from provenance");
|
||
|
|
assert!(frontmatter.contains("generated_at:"), "Should have generated_at timestamp");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a5_skill_draft_includes_provenance() {
|
||
|
|
let frontmatter = r#"---
|
||
|
|
name: test-skill
|
||
|
|
generated_from: "sha256_abc123def456"
|
||
|
|
---
|
||
|
|
"#;
|
||
|
|
|
||
|
|
assert!(frontmatter.contains("generated_from:"), "Frontmatter must have generated_from");
|
||
|
|
assert!(frontmatter.contains("sha256_abc123def456"), "Provenance should contain sha256");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a6_skill_draft_enforces_drafts_directory() {
|
||
|
|
// Skills must be written to _drafts/, not directly to skills/
|
||
|
|
let valid_path = "vault/skills/_drafts/skill-name/SKILL.md";
|
||
|
|
let invalid_path = "vault/skills/skill-name/SKILL.md"; // Would be promoted, not draft
|
||
|
|
|
||
|
|
assert!(valid_path.contains("_drafts"), "Draft must go to _drafts directory");
|
||
|
|
assert!(!invalid_path.contains("_drafts"), "Promoted skills should not have _drafts");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a7_skill_draft_dry_run_no_write() {
|
||
|
|
// Dry run should not create files
|
||
|
|
let test_dir = "vault/skills/_drafts/test-a7-dryrun";
|
||
|
|
|
||
|
|
// Clean up first
|
||
|
|
let _ = fs::remove_dir_all(test_dir);
|
||
|
|
|
||
|
|
// Simulate dry run (no actual write)
|
||
|
|
let dry_run = true;
|
||
|
|
|
||
|
|
if dry_run {
|
||
|
|
// Would print but not write
|
||
|
|
assert!(!Path::new(test_dir).exists(), "Dry run should not create directory");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify directory was not created
|
||
|
|
assert!(!Path::new(test_dir).exists(), "Directory should not exist after dry-run");
|
||
|
|
}
|