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 includingassert_blocks,assert_allows, andassert_matches_within_budget, plus the optionalLoggedPackTestRunnerfor JSON CI reporting.mod.rs– The registry where completed packs are registered withREGISTRY.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
idfollows the required lowercase format with only.,_, or digits nameanddescriptionare non-empty strings- At least one keyword exists in the
keywordsarray - 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>.rsand expose acreate_pack()function returning a configuredPackstruct. - Test helpers in
src/packs/test_helpers.rsprovide assertions likeassert_blocks,assert_allows,assert_matches_within_budget, andvalidate_packto 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)insrc/packs/mod.rsto make the pack active in the evaluator. - Execution uses
cargo test packs::<category>::<name>for focused testing or./scripts/scan_precommit_e2e.shfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →