Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -664,12 +664,18 @@ fn git_sync_failure(message: &str, output: &Output) -> String {
)
}

fn assert_mounted_git_sync(output: &Output, dry_run: bool) {
fn assert_mounted_git_sync(output: &Output, dry_run: bool, daemon_log: &Path) {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"sessions git-sync should succeed\nstdout:\n{stdout}\nstderr:\n{stderr}"
"sessions git-sync should succeed\nstdout:\n{stdout}\nstderr:\n{stderr}\ntopology diagnostics:\n{}",
std::fs::read_to_string(daemon_log)
.expect("read isolated daemon log")
.lines()
.filter(|line| line.contains("git_topology_publication_unavailable"))
.collect::<Vec<_>>()
.join("\n")
);
assert!(
stdout.starts_with("session git sync completed (session-sync."),
Expand Down Expand Up @@ -723,10 +729,13 @@ fn sessions_git_sync_on_a_cold_daemon_waits_for_the_project_mount() {
let hold = canonical_temp_path(home.path()).join("hold-after-core-publish");
std::fs::write(&hold, b"hold").unwrap();
let entered = PathBuf::from(format!("{}.entered", hold.display()));
let _daemon = crate::common::spawn_tracedecay_daemon_with(home.path(), {
let daemon_log = home.path().join("git-sync-daemon.log");
let _daemon = crate::common::spawn_tracedecay_daemon_logged(home.path(), &daemon_log, {
let hold = hold.clone();
move |command| {
command.env("TRACEDECAY_TEST_HOLD_AFTER_CORE_PUBLISH", &hold);
command
.env("TRACEDECAY_TEST_HOLD_AFTER_CORE_PUBLISH", &hold)
.env("RUST_LOG", "warn");
}
});

Expand Down Expand Up @@ -775,11 +784,16 @@ fn sessions_git_sync_on_a_cold_daemon_waits_for_the_project_mount() {
assert_mounted_git_sync(
&wait_for_git_sync(&mut child, &mut stdout, &mut stderr),
true,
&daemon_log,
);

let mut command = tracedecay_command_without_daemon(home.path(), &project_root);
command.args(["sessions", "git-sync"]);
assert_mounted_git_sync(&run_with_timeout(command, cli_timeout()), false);
assert_mounted_git_sync(
&run_with_timeout(command, cli_timeout()),
false,
&daemon_log,
);
}

fn dashboard_session_authority(capabilities_url: &str) -> String {
Expand Down
264 changes: 256 additions & 8 deletions crates/tracedecay-maintenance/src/retention/footprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,26 @@ fn floor_note(unavailable_entry_count: usize, truncated: bool) -> String {
}
}

/// Only an enrolled project has a first-party destructive reclaim path.
/// An unregistered directory or unrelated profile family must never be sent
/// through `projects forget`; it only accepts registered identities.
fn registered_project_reclaim_hint(entry: &ProfileFootprintEntryV1) -> Option<String> {
if entry.family != ProfileFamilyV1::ProjectStore || entry.bytes <= entry.bound_bytes {
return None;
}
entry.project.as_ref()?.registered_root.as_ref()?;
let id = entry.key.strip_prefix("projects/").filter(|id| {
id.starts_with("proj_")
&& id.len() > "proj_".len()
&& id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
})?;
Some(format!(
"; if retention stalls, LAST RESORT (DELETES project data): `tracedecay --dry-run projects forget {id}` then `tracedecay --yes projects forget {id}`"
))
}

fn entry_statement(entry: &ProfileFootprintEntryV1) -> String {
let label = match entry.family {
ProfileFamilyV1::ProjectStore => "project store",
Expand Down Expand Up @@ -609,10 +629,46 @@ fn entry_statement(entry: &ProfileFootprintEntryV1) -> String {
format_bytes(kinds.graph_replay_pool)
)
});
let reclaim = registered_project_reclaim_hint(entry).unwrap_or_default();
let floor = floor_note(entry.unavailable_entry_count, false);
let render = |root: &str, reclaim: &str| {
format!("{label} {}{reclaim}{root}{breakdown}{floor}", entry.key)
};
let fits = |subject: &str| {
let statement = footprint_statement(subject, entry.bytes, entry.bound_bytes);
super::diagnostics::bounded_statement(&statement) == statement
};
let detailed = render(&root, &reclaim);
if reclaim.is_empty() || fits(&detailed) {
return detailed;
}
// Budget the complete rendered finding, including both commands, the
// deletion warning, and the measured overage. A root-length heuristic
// does not account for selector length, UTF-8, or the other evidence.
let root_state = entry
.project
.as_ref()
.map(|project| format!(" ({})", project.state.label()))
.unwrap_or_default();
let compact = render(
&format!(" (registered root omitted from bounded diagnostic){root_state}"),
&reclaim,
);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
if fits(&compact) {
compact
} else {
// Never turn a long identity into a truncated destructive command.
// Preserve the root's liveness and measured pressure if the complete
// recovery command cannot fit within the diagnostic limit.
render(&root_state, "")
}
}

fn footprint_statement(subject: &str, bytes: u64, bound_bytes: u64) -> String {
format!(
"{label} {}{root}{breakdown}{}",
entry.key,
floor_note(entry.unavailable_entry_count, false)
"{subject}: {} of {} bound",
format_bytes(bytes),
format_bytes(bound_bytes)
)
}

Expand All @@ -639,11 +695,7 @@ fn bounded_finding(
DoctorCoverageCompletenessV1::Complete,
)
};
let statement = format!(
"{subject}: {} of {} bound",
format_bytes(bytes),
format_bytes(bound_bytes)
);
let statement = footprint_statement(subject, bytes, bound_bytes);
let reference = DoctorEvidenceReferenceV1::new(key.to_owned()).ok()?;
let evidence = DoctorEvidenceRefV1::new(DoctorFindingFamilyV1::Storage, reference);
let coverage = DoctorCoverageStatementV1::new(
Expand Down Expand Up @@ -997,6 +1049,202 @@ mod tests {
);
}

#[test]
fn overbound_project_doctor_names_a_supported_destructive_reclaim_only_for_registered_stores() {
let fixture = profile_fixture();
let overrides = BTreeMap::from([
("projects/proj_live".to_owned(), 1024),
("projects/proj_stray".to_owned(), 1),
]);
let mut over = census_profile_footprint(&fixture.profile, &fixture.registered, &overrides);
let over_findings = findings(&over);
let live = &over_findings["projects/proj_live"];
assert_eq!(live.finding().state(), DoctorEvidenceStateV1::Degraded);
let statement = live.finding().coverage().statement();
assert!(
statement.contains("tracedecay --dry-run projects forget proj_live"),
"{statement}"
);
assert!(
statement.contains("tracedecay --yes projects forget proj_live"),
"{statement}"
);
assert!(statement.contains("DELETES project data"), "{statement}");

let unregistered = &over_findings["projects/proj_stray"];
assert_eq!(
unregistered.finding().state(),
DoctorEvidenceStateV1::Degraded
);
assert!(
!unregistered
.finding()
.coverage()
.statement()
.contains("projects forget"),
"an unregistered directory has no safe project selector"
);
assert!(
!over_findings["profile"]
.finding()
.coverage()
.statement()
.contains("projects forget"),
"a profile total does not identify a reclaimable project"
);

let long_root = "nested-repository/".repeat(70);
let registered = over
.entries
.iter_mut()
.find(|entry| entry.key == "projects/proj_live")
.expect("registered footprint");
registered
.project
.as_mut()
.expect("registered project")
.registered_root = Some(long_root);
let bounded = findings(&over);
let bounded_statement = bounded["projects/proj_live"]
.finding()
.coverage()
.statement();
assert!(
bounded_statement.contains("tracedecay --dry-run projects forget proj_live"),
"{bounded_statement}"
);
assert!(
bounded_statement.contains(" of 1.0 KB bound"),
"over-budget bytes and limit must survive a long repository path: {bounded_statement}"
);
assert!(bounded_statement.contains("registered root omitted"));

over.entries
.iter_mut()
.find(|entry| entry.key == "projects/proj_live")
.expect("registered footprint")
.project
.as_mut()
.expect("registered project")
.state = ProjectRootStateV1::Missing;
let missing_statement = findings(&over)["projects/proj_live"]
.finding()
.coverage()
.statement()
.to_owned();
assert!(
missing_statement.contains("(root missing on disk)"),
"a compact finding must retain a missing root: {missing_statement}"
);
assert!(
missing_statement.contains("tracedecay --dry-run projects forget proj_live"),
"the bounded recovery command must remain intact: {missing_statement}"
);
over.entries
.iter_mut()
.find(|entry| entry.key == "projects/proj_live")
.expect("registered footprint")
.project
.as_mut()
.expect("registered project")
.state = ProjectRootStateV1::Unverifiable;
let unverifiable_statement = findings(&over)["projects/proj_live"]
.finding()
.coverage()
.statement()
.to_owned();
assert!(
unverifiable_statement.contains("(root unverifiable)"),
"a compact finding must retain an unverifiable root: {unverifiable_statement}"
);

let normal =
census_profile_footprint(&fixture.profile, &fixture.registered, &BTreeMap::new());
let healthy = findings(&normal);
assert_eq!(
healthy["projects/proj_live"].finding().state(),
DoctorEvidenceStateV1::HealthyCompleteCoverage
);
assert!(
!healthy["projects/proj_live"]
.finding()
.coverage()
.statement()
.contains("projects forget"),
"within-budget stores must not be offered destructive recovery"
);
}

#[test]
fn reclaim_guidance_omits_unusable_project_selectors() {
let fixture = profile_fixture();
for id in [
"proj_".to_owned(),
"proj_bad;echo".to_owned(),
"proj_bad`id".to_owned(),
format!("proj_{}", "a".repeat(160)),
] {
let key = format!("projects/{id}");
let store = fixture.profile.join(&key).join("tracedecay.db");
sized(&store, 2048);
let registered = BTreeMap::from([(id, fixture.live_root.clone())]);
let overrides = BTreeMap::from([(key.clone(), 1)]);
let footprint = census_profile_footprint(&fixture.profile, &registered, &overrides);
let observed = findings(&footprint);
let finding = &observed[&key];
assert_eq!(finding.finding().state(), DoctorEvidenceStateV1::Degraded);
let statement = finding.finding().coverage().statement();
assert!(
!statement.contains("projects forget"),
"unsafe or incomplete actions must be omitted: {statement}"
);
assert!(statement.contains(" of 1 B bound"), "{statement}");
assert_eq!(
std::fs::metadata(&store).expect("unchanged store").len(),
2048
);
}
}

#[test]
fn reclaim_guidance_preserves_real_identity_commands_and_large_store_evidence() {
let temporary = tempfile::tempdir().expect("large footprint fixture");
let profile = temporary.path().join("profile");
let id = "proj_0123456789abcdef";
let key = format!("projects/{id}");
let store = profile.join(&key).join("tracedecay.db");
// Sparse metadata-only input: no allocation or read of the 15 GiB body.
sized(&store, 15 * GIB);
let registered = BTreeMap::from([(id.to_owned(), "界/".repeat(100))]);
let footprint = census_profile_footprint(&profile, &registered, &BTreeMap::new());
let observed = findings(&footprint);
let finding = &observed[&key];
assert_eq!(finding.finding().state(), DoctorEvidenceStateV1::Degraded);
let statement = finding.finding().coverage().statement();
assert!(
statement.contains(&format!("`tracedecay --dry-run projects forget {id}`")),
"{statement}"
);
assert!(
statement.contains(&format!("`tracedecay --yes projects forget {id}`")),
"{statement}"
);
assert!(statement.contains("DELETES project data"), "{statement}");
assert!(statement.contains("registered root omitted"), "{statement}");
assert!(
statement.ends_with(&format!(
"{} of {} bound",
format_bytes(15 * GIB),
format_bytes(default_bound_bytes(ProfileFamilyV1::ProjectStore))
)),
"{statement}"
);
assert_eq!(
std::fs::metadata(&store).expect("unchanged store").len(),
15 * GIB
);
}

#[test]
fn an_owner_override_replaces_the_default_bound() {
let fixture = profile_fixture();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ impl SessionSyncProjectContext {
&self.project_id,
) {
Ok(scope) => scope,
Err(_) => {
return GitTopologySyncOutcome::Finished(Err(GitTopologySyncFailure::Unavailable));
Err(error) => {
return GitTopologySyncOutcome::Finished(Err(
unavailable("resolve project scope")(error),
));
}
};
let Some(runtime) = project_sessions.project_graph_runtime() else {
Expand All @@ -72,8 +74,10 @@ impl SessionSyncProjectContext {
let runtime: Arc<dyn VerifiedGraphRuntimePortV1> = Arc::new(runtime.clone());
let scope_sets = match project_sessions.authorized_scope_set_storage() {
Ok(scope_sets) => scope_sets,
Err(_) => {
return GitTopologySyncOutcome::Finished(Err(GitTopologySyncFailure::Unavailable));
Err(error) => {
return GitTopologySyncOutcome::Finished(Err(unavailable(
"open scope-set storage",
)(error)));
}
};
let project_root = self.project_root.clone();
Expand All @@ -97,7 +101,7 @@ impl SessionSyncProjectContext {
result = &mut worker => {
GitTopologySyncOutcome::Finished(match result {
Ok(result) => result,
Err(_) => Err(GitTopologySyncFailure::Unavailable),
Err(error) => Err(unavailable("join topology publisher")(error)),
})
}
interruption = service.wait_for_interruption(request) => {
Expand Down
Loading