How to Write Pack Tests and Validate Custom Packs for dcg

To write pack tests for dcg, create a test module that imports helpers from src/packs/test_helpers.rs, implements tests for pack creation, destructive patterns, safe patterns, and performance, then register the pack in src/packs/mod.rs and run cargo test to validate.

Destructive Command Guard (dcg) is an open-source Rust project that protects AI agents by organizing regex patterns into modular packs. Writing comprehensive tests for these packs ensures that destructive operations are correctly identified and blocked while legitimate commands remain accessible. This guide demonstrates exactly how to write pack tests and validate custom packs for dcg using the repository's built-in testing framework and reusable validation helpers.

Understanding the dcg Testing Architecture

The dcg testing architecture consists of three reusable components located in src/packs/:

  • test_template.rs – A complete skeleton showing the required test sections and structure for any new pack.
  • test_helpers.rs – A library of assertion functions including assert_blocks, assert_allows, and assert_matches_within_budget, plus the optional LoggedPackTestRunner for JSON CI reporting.
  • mod.rs – The registry where completed packs are registered with REGISTRY.register(pack) to make them available to the evaluator.

Each pack lives at src/packs/<category>/<name>.rs and requires a corresponding test suite, either embedded in the same file within a #[cfg(test)] mod tests { ... } block or in a dedicated src/packs/<category>/<name>_tests.rs file.

Step 1: Create the Pack Module

Begin by implementing the create_pack() function in your pack file. This function must return a Pack struct populated with metadata and pattern definitions using the provided macros:

// src/packs/database/postgresql.rs
pub fn create_pack() -> Pack {
    Pack {
        id: "database.postgresql".to_string(),
        name: "PostgreSQL",
        description: "Blocks destructive PostgreSQL commands (DROP DATABASE, TRUNCATE).",
        keywords: &["psql", "postgres"],
        safe_patterns: vec![
            safe_pattern!("list-databases", r"(?:^|\s)psql\s+...?\s+-l"),
        ],
        destructive_patterns: vec![
            destructive_pattern!(
                "drop-database",
                r"(?:^|\s)psql\s+...?\s+DROP\s+DATABASE",
                "DROP DATABASE permanently erases a DB. Use pg_dump first.",
                Critical,
                &const {
                    [
                        PatternSuggestion::new("pg_dump", "Export the DB before dropping"),
                    ]
                }
            ),
        ],
        keyword_matcher: None,
        safe_regex_set: None,
        safe_regex_set_is_complete: false,
    }
}

The id must follow the format lowercase with only dots, underscores, and digits. Each destructive pattern requires a unique name, regex, human-readable reason, severity level (Critical, High, Medium, or Low), and optional suggestions.

Step 2: Set Up the Test File

Create your test module by copying the structure from src/packs/test_template.rs. Replace the placeholder references with your pack's module path:

// src/packs/database/postgresql_tests.rs
#[cfg(test)]
mod tests {
    use super::*;
    use crate::packs::test_helpers::*;
    use crate::packs::Severity;

    // Tests will go here following the template sections
}

Alternatively, embed the tests directly in postgresql.rs using #[cfg(test)] mod tests { ... }.

Step 3: Write Pack Creation Tests

Validate that your pack initializes correctly and meets all structural requirements using the validate_pack helper:

#[test]
fn test_pack_creation() {
    let pack = create_pack();
    validate_pack(&pack);
}

The validate_pack function automatically verifies that:

  • The id follows the required lowercase format with only ., _, or digits
  • name and description are non-empty strings
  • At least one keyword exists in the keywords array
  • All regex patterns compile successfully
  • Every destructive pattern has a non-empty reason string
  • Pattern names are unique within the pack

Step 4: Test Destructive Patterns

Write individual tests for each destructive pattern to ensure commands are blocked with correct severity and metadata:

#[test]
fn test_drop_database_critical() {
    let pack = create_pack();
    
    // Verify the command is blocked with reason substring match
    assert_blocks(&pack, "psql -c \"DROP DATABASE secret\"",
                  "DROP DATABASE permanently erases");
    
    // Verify severity level
    assert_blocks_with_severity(&pack,
                                "psql -c \"DROP DATABASE secret\"",
                                Severity::Critical);
    
    // Verify specific pattern triggered
    assert_blocks_with_pattern(&pack,
                               "psql -c \"DROP DATABASE secret\"",
                               "drop-database");
}

Use assert_blocks_with_pattern to confirm the specific pattern name matches, and assert_blocks_with_severity to validate the assigned severity level.

Step 5: Test Safe Patterns and Specificity

Ensure that safe patterns match intended commands and that unrelated commands do not trigger false positives:

#[test]
fn test_safe_list_allows() {
    let pack = create_pack();
    assert_safe_pattern_matches(&pack, "psql -l");
    assert_allows(&pack, "psql -c \"SELECT * FROM users\"");
}

#[test]
fn test_specificity_unrelated() {
    let pack = create_pack();
    // Ensure MySQL commands don't trigger PostgreSQL patterns
    assert_no_match(&pack, "mysqldump -u root");
}

The assert_allows helper confirms the pack does not block benign commands, while assert_no_match verifies that completely unrelated commands bypass the pack entirely.

Step 6: Add Performance and Edge-Case Tests

Guard against catastrophic regex backtracking by testing execution time budgets:

#[test]
fn test_performance() {
    let pack = create_pack();
    assert_matches_within_budget(&pack,
                                 "psql -c \"DROP DATABASE secret\"");
}

Add edge-case tests for whitespace variations, quoted arguments, special characters, and empty strings. For batch validation, use test_batch_blocks or test_batch_allows to validate multiple commands in a single test.

For CI integration, instantiate LoggedPackTestRunner to produce deterministic JSON output:

#[test]
fn test_with_logging() {
    let pack = create_pack();
    let runner = LoggedPackTestRunner::debug(&pack);
    runner.assert_blocks("psql -c \"DROP DATABASE secret\"");
    runner.assert_allows("psql -l");
    runner.finish(); // Outputs JSON report for scripts/e2e_test.sh
}

Step 7: Register the Pack and Run Tests

Add your pack to the registry in src/packs/mod.rs:

REGISTRY.register(database::postgresql::create_pack());

Execute the test suite for your specific pack:

cargo test packs::database::postgresql

The test harness automatically warms up lazy regexes and measures execution time. For full validation including integration tests, run the pre-commit script:

./scripts/scan_precommit_e2e.sh

This ensures your new pack does not break existing functionality and validates all destructive patterns against the full command suite.

Summary

  • Pack modules live at src/packs/<category>/<name>.rs and expose a create_pack() function returning a configured Pack struct.
  • Test helpers in src/packs/test_helpers.rs provide assertions like assert_blocks, assert_allows, assert_matches_within_budget, and validate_pack to enforce correctness and performance.
  • Test structure follows the template in src/packs/test_template.rs, covering pack creation validation, destructive pattern matching, safe pattern verification, specificity testing, and performance budgets.
  • Registration requires adding REGISTRY.register(pack) in src/packs/mod.rs to make the pack active in the evaluator.
  • Execution uses cargo test packs::<category>::<name> for focused testing or ./scripts/scan_precommit_e2e.sh for full CI validation.

Frequently Asked Questions

Where should I place test files for a new dcg pack?

You can either embed tests directly in the pack file using #[cfg(test)] mod tests { ... } or create a separate file at src/packs/<category>/<name>_tests.rs. Both approaches are valid; choose based on whether the test logic is extensive enough to warrant separation from the implementation.

How do I verify that my pack correctly identifies destructive commands?

Use the assert_blocks helper to verify commands trigger blocks, assert_blocks_with_pattern to confirm the specific pattern name matches, and assert_blocks_with_severity to validate the severity level (Critical, High, Medium, or Low). Always include tests that verify the reason string contains expected explanatory text.

What performance requirements must dcg custom packs meet?

Every pack should use assert_matches_within_budget to prevent regex patterns with catastrophic backtracking from impacting the evaluator. This helper fails the test if pattern matching exceeds the defined execution time budget, ensuring packs remain fast enough for real-time command guarding.

How do I integrate pack tests into CI/CD pipelines?

Import LoggedPackTestRunner from src/packs/test_helpers.rs to generate JSON test reports that CI systems can parse. The repository includes ./scripts/scan_precommit_e2e.sh which consumes these reports to detect regressions automatically without relying on flaky string matching, providing deterministic pass/fail signals for deployment gates.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →