use std::{
fs,
path::{Path, PathBuf},
};
const ALLOWED_ENVELOPE_FILE: &str = "cli/commands/error_envelope.rs";
const ALLOWED_ADVICE_FILE: &str = "cli/commands/advice.rs";
const ALLOWED_HISTORY_TARGET_FILE: &str = "cli/commands/history_target.rs";
const ALLOWED_NEXT_ACTION_FILE: &str = "cli/commands/next_action.rs";
const ALLOWED_ACTION_LINE_FILE: &str = "cli/commands/action_line.rs";
const RAW_RECOVERY_PHRASES: &[&str] = &[
"State not found:",
"network fetch support is not available",
"network push support is not available",
"network pull support is not available",
"invalid Git remote name for Git-overlay repository",
"repository has no HEAD; capture a state first",
"Repository has no HEAD state - take a snapshot first",
"Use one path.",
"--principal-name is required",
"--principal-email is required",
"--annotation-kind is required for into-annotation",
"--annotation-content is required for into-annotation",
"--reason is required for dismiss",
"--symbols expects 'file:symbol'",
"has no recorded parent; pass --into",
];
const RAW_THREAD_NOT_FOUND_PHRASES: &[&str] = &[
"Thread not found: {thread}",
"Thread not found: {}",
"Thread '{}' not found",
"Target thread '{}' not found",
"Thread '{}' not found after capture",
"Thread '{}' not found after refresh",
];
#[test]
fn error_envelopes_stay_centralized() {
let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut violations = Vec::new();
walk_rust_files(&src_dir, &mut |path| {
let rel = path.strip_prefix(&src_dir).unwrap_or(path);
if rel == Path::new(ALLOWED_ENVELOPE_FILE) {
return;
}
let Ok(source) = fs::read_to_string(path) else {
return;
};
for (line_index, line) in source.lines().enumerate() {
if line.contains("eprintln!(\"Error:")
|| line.contains("eprintln!(\"Hint:")
|| line.contains("\"code\": \"parse_error\"")
|| line.contains("\"kind\": \"parse_error\"")
|| line.contains("\"code\": kind")
|| line.contains("\"kind\": kind")
{
violations.push(format!("{}:{}", rel.display(), line_index + 1));
}
}
});
assert!(
violations.is_empty(),
"error envelope rendering must stay centralized in {ALLOWED_ENVELOPE_FILE}; violations:\n{}",
violations.join("\n")
);
}
#[test]
fn known_recovery_phrases_stay_in_typed_advice() {
let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut violations = Vec::new();
walk_rust_files(&src_dir, &mut |path| {
let rel = path.strip_prefix(&src_dir).unwrap_or(path);
let Ok(source) = fs::read_to_string(path) else {
return;
};
for (line_index, line) in source.lines().enumerate() {
for phrase in RAW_RECOVERY_PHRASES {
if recovery_phrase_allowed(rel, phrase) {
continue;
}
if line.contains(phrase) {
violations.push(format!(
"{}:{} contains raw recovery phrase `{phrase}`",
rel.display(),
line_index + 1
));
}
}
for phrase in RAW_THREAD_NOT_FOUND_PHRASES {
if recovery_phrase_allowed(rel, phrase) {
continue;
}
if line.contains(phrase) {
violations.push(format!(
"{}:{} contains raw missing-thread phrase `{phrase}`",
rel.display(),
line_index + 1
));
}
}
}
});
assert!(
violations.is_empty(),
"recovery phrases must be emitted through typed RecoveryAdvice constructors; violations:\n{}",
violations.join("\n")
);
}
#[test]
fn git_bridge_recovery_policy_stays_out_of_error_renderer() {
let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let envelope = src_dir.join(ALLOWED_ENVELOPE_FILE);
let source = fs::read_to_string(&envelope)
.unwrap_or_else(|err| panic!("read {}: {err}", envelope.display()));
assert!(
source.contains("RecoveryAdvice::from_git_bridge_error"),
"{ALLOWED_ENVELOPE_FILE} should delegate GitBridgeError policy to typed advice"
);
for forbidden in [
"NonFastForwardRef",
"GitHeddleThreadDiverged",
"RemoteDiverged",
"ShallowClone",
"refs/notes/heddle",
"git_overlay_remote_diverged",
"git_overlay_mapping_conflict",
"git_overlay_shallow_clone",
] {
assert!(
!source.contains(forbidden),
"{ALLOWED_ENVELOPE_FILE} should render Git bridge recovery advice, not own policy `{forbidden}`"
);
}
}
#[test]
fn remote_recovery_policy_uses_typed_advice_constructors() {
let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut violations = Vec::new();
for (file, forbidden) in [
(
"cli/commands/remote/mod.rs",
&[
"remote_transport_mismatch_advice",
"remote_not_configured_advice",
"git_tracking_refresh_failed_advice",
"network_push_failed_advice",
][..],
),
(
"cli/commands/remote/remote_ops.rs",
&[
"local_lazy_pull_advice",
"network_pull_failed_advice",
"remote_not_found_advice",
][..],
),
(
"cli/commands/fetch.rs",
&["fetch_remote_required_advice"][..],
),
(
"cli/commands/clone.rs",
&["network_clone_failed_advice"][..],
),
] {
let path = src_dir.join(file);
let source = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
for symbol in forbidden {
if source.contains(symbol) {
violations.push(format!("{file} contains `{symbol}`"));
}
}
}
assert!(
violations.is_empty(),
"remote recovery policy should live on RecoveryAdvice constructors:\n{}",
violations.join("\n")
);
}
#[test]
fn verification_blocked_outputs_use_shared_action_policy() {
let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut violations = Vec::new();
for file in [
"cli/commands/operator_loop.rs",
"cli/commands/thread_shaping.rs",
"cli/commands/merge/mod.rs",
"cli/commands/ready_cmd.rs",
"cli/commands/workflow.rs",
"cli/commands/rebase/mod.rs",
] {
let path = src_dir.join(file);
let source = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
if source.contains("trust.recommended_action.is_empty()") {
violations.push(format!(
"{file} reimplements repository verification recommended-action fallback"
));
}
}
assert!(
violations.is_empty(),
"repository-verification blocked outputs should use repository_verification_primary_command or OperatorCommandOutput::blocked_by_repository_verification:\n{}",
violations.join("\n")
);
}
#[test]
fn next_action_priority_lives_in_shared_selector() {
let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut violations = Vec::new();
walk_rust_files(&src_dir, &mut |path| {
let rel = path.strip_prefix(&src_dir).unwrap_or(path);
if rel == Path::new(ALLOWED_NEXT_ACTION_FILE) {
return;
}
let Ok(source) = fs::read_to_string(path) else {
return;
};
for (line_index, line) in source.lines().enumerate() {
for fragment in [
"remote_tracking.behind > 0",
"heddle bridge git import --ref {}",
"thread_action.filter(|action| !action.trim().is_empty())",
] {
if line.contains(fragment) {
violations.push(format!(
"{}:{} reimplements next-action priority fragment `{fragment}`",
rel.display(),
line_index + 1
));
}
}
}
});
assert!(
violations.is_empty(),
"next-action priority should be selected through {ALLOWED_NEXT_ACTION_FILE}; violations:\n{}",
violations.join("\n")
);
}
#[test]
fn human_action_lines_use_shared_renderer() {
let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut violations = Vec::new();
walk_rust_files(&src_dir, &mut |path| {
let rel = path.strip_prefix(&src_dir).unwrap_or(path);
if rel == Path::new(ALLOWED_ACTION_LINE_FILE) || rel == Path::new(ALLOWED_ENVELOPE_FILE) {
return;
}
let Ok(source) = fs::read_to_string(path) else {
return;
};
for (line_index, line) in source.lines().enumerate() {
let trimmed = line.trim_start();
for fragment in [
"println!(\"Next:",
"println!(\"Next step:",
"println!(\"Optional:",
"println!(\" command:",
"println!(\" Next:",
"println!(\" next step:",
"println!(\" optional:",
"writeln!(out, \"Next step:",
"format!(\" Next:",
] {
if trimmed.starts_with(fragment) {
violations.push(format!(
"{}:{} renders action line `{fragment}` outside {ALLOWED_ACTION_LINE_FILE}",
rel.display(),
line_index + 1
));
}
}
}
});
assert!(
violations.is_empty(),
"human action lines should use {ALLOWED_ACTION_LINE_FILE}; violations:\n{}",
violations.join("\n")
);
}
#[test]
fn git_overlay_mutation_preflight_stays_shared() {
let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut violations = Vec::new();
for (file, forbidden) in [
(
"cli/commands/git_compat.rs",
&[
"plain_git_mutation_advice(",
"detached_git_head_mutation_advice(",
"unimported_git_history_advice(&repo, \"commit\")",
"raw_git_operation_mutation_advice(",
"verification_blocking_mutation_advice(",
][..],
),
(
"cli/commands/checkpoint.rs",
&[
"plain_git_mutation_advice(",
"detached_git_head_mutation_advice(",
"unimported_git_history_advice(repo, \"checkpoint\")",
"verification_blocking_mutation_advice(",
][..],
),
] {
let path = src_dir.join(file);
let source = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
for fragment in forbidden {
if source.contains(fragment) {
violations.push(format!(
"{file} reimplements mutation preflight `{fragment}`"
));
}
}
}
assert!(
violations.is_empty(),
"Git-overlay mutation safety preflight should flow through git_overlay_mutation_preflight_advice/plain_git_mutation_preflight_advice:\n{}",
violations.join("\n")
);
}
fn recovery_phrase_allowed(rel: &Path, phrase: &str) -> bool {
rel == Path::new(ALLOWED_ADVICE_FILE)
|| rel == Path::new(ALLOWED_ENVELOPE_FILE)
|| (phrase == "State not found:" && rel == Path::new(ALLOWED_HISTORY_TARGET_FILE))
}
fn walk_rust_files(dir: &Path, visit: &mut dyn FnMut(&Path)) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
if name.starts_with('.') {
continue;
}
if path.is_dir() {
walk_rust_files(&path, visit);
} else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
visit(&path);
}
}
}