Files
owlen/crates/tools/slash/tests/slash_command.rs
vikingowl 5134462deb feat(tools): implement Slash Commands with frontmatter and file refs (M5 complete)
This commit implements the complete M5 milestone (Slash Commands) including:

Slash Command Parser (tools-slash):
- YAML frontmatter parsing with serde_yaml
- Metadata extraction (description, author, tags, version)
- Arbitrary frontmatter fields via flattened HashMap
- Graceful fallback for commands without frontmatter

Argument Substitution:
- $ARGUMENTS - all arguments joined by space
- $1, $2, $3, etc. - positional arguments
- Unmatched placeholders remain unchanged
- Empty arguments result in empty string for $ARGUMENTS

File Reference Resolution:
- @path syntax to include file contents inline
- Regex-based matching for file references
- Multiple file references supported
- Clear error messages for missing files

CLI Integration:
- Added `slash` subcommand: `owlen slash <command> <args...>`
- Loads commands from `.claude/commands/<name>.md`
- Permission checks for SlashCommand tool
- Automatic file reference resolution before output

Command Structure:
---
description: "Command description"
author: "Author name"
tags:
  - tag1
  - tag2
---
Command body with $ARGUMENTS and @file.txt references

Permission Enforcement:
- Plan mode: SlashCommand allowed (utility tool)
- All modes: SlashCommand respects permissions
- File references respect filesystem permissions

Testing:
- 10 tests in tools-slash for parser functionality
  - Frontmatter parsing with complex YAML
  - Argument substitution (all variants)
  - File reference resolution (single and multiple)
  - Edge cases (no frontmatter, empty args, etc.)
- 3 new tests in CLI for integration
  - slash_command_works (with args and frontmatter)
  - slash_command_file_refs (file inclusion)
  - slash_command_not_found (error handling)
- All 56 workspace tests passing 

Dependencies Added:
- serde_yaml 0.9 for YAML frontmatter parsing
- regex 1.12 for file reference pattern matching

M5 milestone complete! 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 19:41:42 +01:00

110 lines
3.1 KiB
Rust

use tools_slash::parse_slash_command;
use std::fs;
use tempfile::tempdir;
#[test]
fn slash_parse_frontmatter_and_args() {
let content = r#"---
description: "Test command"
author: "Test Author"
---
This is the command body with $ARGUMENTS
First arg: $1
Second arg: $2
"#;
let cmd = parse_slash_command(content, &["arg1", "arg2"]).unwrap();
assert_eq!(cmd.description, Some("Test command".to_string()));
assert_eq!(cmd.author, Some("Test Author".to_string()));
assert!(cmd.body.contains("arg1 arg2")); // $ARGUMENTS replaced
assert!(cmd.body.contains("First arg: arg1")); // $1 replaced
assert!(cmd.body.contains("Second arg: arg2")); // $2 replaced
}
#[test]
fn slash_parse_no_frontmatter() {
let content = "Simple command without frontmatter";
let cmd = parse_slash_command(content, &[]).unwrap();
assert_eq!(cmd.description, None);
assert_eq!(cmd.author, None);
assert_eq!(cmd.body.trim(), "Simple command without frontmatter");
}
#[test]
fn slash_file_refs() {
let dir = tempdir().unwrap();
let test_file = dir.path().join("test.txt");
fs::write(&test_file, "File content here").unwrap();
let content = format!("Check this file: @{}", test_file.display());
let cmd = parse_slash_command(&content, &[]).unwrap();
let resolved = cmd.resolve_file_refs().unwrap();
assert!(resolved.contains("File content here"));
assert!(!resolved.contains(&format!("@{}", test_file.display())));
}
#[test]
fn slash_arguments_substitution() {
let content = "All args: $ARGUMENTS\nFirst: $1\nSecond: $2\nThird: $3";
let cmd = parse_slash_command(content, &["hello", "world"]).unwrap();
assert!(cmd.body.contains("All args: hello world"));
assert!(cmd.body.contains("First: hello"));
assert!(cmd.body.contains("Second: world"));
assert!(cmd.body.contains("Third: $3")); // No third arg, should remain
}
#[test]
fn slash_multiple_file_refs() {
let dir = tempdir().unwrap();
let file1 = dir.path().join("file1.txt");
let file2 = dir.path().join("file2.txt");
fs::write(&file1, "Content 1").unwrap();
fs::write(&file2, "Content 2").unwrap();
let content = format!("File 1: @{}\nFile 2: @{}", file1.display(), file2.display());
let cmd = parse_slash_command(&content, &[]).unwrap();
let resolved = cmd.resolve_file_refs().unwrap();
assert!(resolved.contains("Content 1"));
assert!(resolved.contains("Content 2"));
}
#[test]
fn slash_empty_args_leaves_placeholders() {
let content = "Args: $ARGUMENTS, First: $1, Second: $2";
let cmd = parse_slash_command(content, &[]).unwrap();
// With no args, $ARGUMENTS becomes empty, but positional args remain
assert!(cmd.body.contains("Args: ,"));
assert!(cmd.body.contains("First: $1"));
assert!(cmd.body.contains("Second: $2"));
}
#[test]
fn slash_complex_frontmatter() {
let content = r#"---
description: "Multi-line
description"
tags:
- test
- example
version: 1.0
---
Command body
"#;
let cmd = parse_slash_command(content, &[]).unwrap();
assert!(cmd.description.is_some());
assert!(cmd.description.as_ref().unwrap().contains("Multi-line"));
}