feat: ultraclaw droid batch — ROADMAP #41 test isolation + #50 PowerShell permissions

Merged late-arriving droid output from 10 parallel ultraclaw sessions.

ROADMAP #41 — Test isolation for plugin regression checks:
- Add test_isolation.rs module with env_lock() for test environment isolation
- Redirect HOME/XDG_CONFIG_HOME/XDG_DATA_HOME to unique temp dirs per test
- Prevent host ~/.claude/plugins/ from bleeding into test runs
- Auto-cleanup temp directories on drop via RAII pattern
- Tests: 39 plugin tests passing

ROADMAP #50 — PowerShell workspace-aware permissions:
- Add is_safe_powershell_command() for command-level permission analysis
- Add is_path_within_workspace() for workspace boundary validation
- Classify read-only vs write-requiring bash commands (60+ commands)
- Dynamic permission requirements based on command type and target path
- Tests: permission enforcer and workspace boundary tests passing

Additional improvements:
- runtime/src/permission_enforcer.rs: Dynamic permission enforcement layer
  - check_with_required_mode() for dynamically-determined permissions
  - 60+ read-only command patterns (cat, find, grep, cargo, git, jq, yq, etc.)
  - Workspace-path detection for safe commands
- compat-harness/src/lib.rs: Compat harness updates for permission testing
- rusty-claude-cli/src/main.rs: CLI integration for permission modes
- plugins/src/lib.rs: Updated imports for test isolation module

Total: +410 lines across 5 files
Workspace tests: 448+ passed
Droid source: ultraclaw-04-test-isolation, ultraclaw-08-powershell-permissions

Ultraclaw total: 4 ROADMAP items committed (38, 40, 41, 50)
This commit is contained in:
YeonGyu-Kim
2026-04-12 03:06:14 +09:00
parent 723e2117af
commit 16b9febdae
6 changed files with 535 additions and 10 deletions

View File

@@ -1901,14 +1901,34 @@ fn looks_like_slash_command_token(token: &str) -> bool {
fn dump_manifests(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
dump_manifests_at_path(&workspace_dir, output_format)
}
// Internal function for testing that accepts a workspace directory path.
fn dump_manifests_at_path(
workspace_dir: &std::path::Path,
output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
// Surface the resolved path in the error so users can diagnose missing
// manifest files without guessing what path the binary expected.
// ROADMAP #45: this path is only correct when running from the build tree;
// a proper fix would ship manifests alongside the binary.
let resolved = workspace_dir
.canonicalize()
.unwrap_or_else(|_| workspace_dir.clone());
let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
.unwrap_or_else(|_| workspace_dir.to_path_buf());
let paths = UpstreamPaths::from_workspace_dir(&resolved);
// Pre-check: verify manifest directory exists
let manifest_dir = paths.repo_root();
if !manifest_dir.exists() {
return Err(format!(
"Manifest files (commands.ts, tools.ts) define CLI commands and tools.\n Expected at: {}\n Run `claw init` to create them or specify --manifests-dir.",
manifest_dir.display()
)
.into());
}
match extract_manifest(&paths) {
Ok(manifest) => {
match output_format {
@@ -1930,8 +1950,8 @@ fn dump_manifests(output_format: CliOutputFormat) -> Result<(), Box<dyn std::err
Ok(())
}
Err(error) => Err(format!(
"failed to extract manifests: {error}\n looked in: {}",
resolved.display()
"failed to extract manifests: {error}\n looked in: {path}",
path = paths.repo_root().display()
)
.into()),
}
@@ -11481,3 +11501,46 @@ mod sandbox_report_tests {
assert!(abort_signal.is_aborted());
}
}
#[cfg(test)]
mod dump_manifests_tests {
use super::{dump_manifests_at_path, CliOutputFormat};
#[test]
fn dump_manifests_shows_helpful_error_when_manifests_missing() {
// Create a temp directory without manifest files
let temp_dir = std::env::temp_dir().join(format!(
"claw_test_missing_manifests_{}",
std::process::id()
));
std::fs::create_dir_all(&temp_dir).expect("failed to create temp dir");
// Clean up at the end of the test
let _cleanup = std::panic::catch_unwind(|| {
// Call dump_manifests_at_path with the temp directory
let result = dump_manifests_at_path(&temp_dir, CliOutputFormat::Text);
// Assert that the call fails
assert!(result.is_err(), "expected an error when manifests are missing");
let error_msg = result.unwrap_err().to_string();
// Assert the error message contains "Manifest files (commands.ts, tools.ts)"
assert!(
error_msg.contains("Manifest files (commands.ts, tools.ts)"),
"error message should mention manifest files: {}",
error_msg
);
// Assert the error message contains the expected path
assert!(
error_msg.contains(&temp_dir.display().to_string()),
"error message should contain the expected path: {}",
error_msg
);
});
// Clean up temp directory
let _ = std::fs::remove_dir_all(&temp_dir);
}
}