1use std::collections::BTreeMap;
2use std::fs;
3use std::io;
4use std::path::{Component, Path, PathBuf};
5use std::time::{Duration, SystemTime};
6
7use crate::db::{SqliteStore, TrackedConnection};
8#[cfg(test)]
9use rusqlite::Connection;
10use rusqlite::OpenFlags;
11use serde_json::json;
12
13const ROOT_KEYED_COPY_DISK_FLOOR_NUMERATOR: u64 = 3;
14const ROOT_KEYED_COPY_DISK_FLOOR_DENOMINATOR: u64 = 2;
15const SQLITE_SUFFIXES: [&str; 4] = [".sqlite-wal", ".sqlite-shm", ".sqlite-journal", ".sqlite"];
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum LegacyPartitionKind {
19 Callgraph,
20 Inspect,
21}
22
23impl LegacyPartitionKind {
24 pub fn as_str(self) -> &'static str {
25 match self {
26 Self::Callgraph => "callgraph",
27 Self::Inspect => "inspect",
28 }
29 }
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct LegacyPartitionInventoryEntry {
34 pub harness: String,
35 pub kind: LegacyPartitionKind,
36 pub key: String,
37 pub path: PathBuf,
40 pub bytes: u64,
41 pub callgraph_pointer_mtime: Option<SystemTime>,
42 pub inspect_tier2_last_full_run: Option<i64>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct LegacyHarnessDuplication {
47 pub harness: String,
48 pub partitions: usize,
49 pub bytes: u64,
50}
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub struct DiskFloorDecision {
54 pub source_bytes: u64,
55 pub available_bytes: u64,
56 pub required_bytes: u64,
57}
58
59impl DiskFloorDecision {
60 pub fn allows_copy(self) -> bool {
61 self.available_bytes >= self.required_bytes
62 }
63
64 pub fn should_skip_copy(self) -> bool {
65 !self.allows_copy()
66 }
67
68 pub fn warning_message(self, source: &Path, target: &Path) -> String {
69 format!(
70 "Skipping root-keyed cache copy from {} into {}: free disk ({}) is below the required 1.5× floor ({} for {} source bytes).",
71 source.display(),
72 target.display(),
73 self.available_bytes,
74 self.required_bytes,
75 self.source_bytes
76 )
77 }
78
79 pub fn configure_warning(self, source: &Path, target: &Path) -> serde_json::Value {
80 json!({
81 "kind": "root_keyed_disk_floor",
82 "source_path": source.display().to_string(),
83 "target_path": target.display().to_string(),
84 "bytes_source": self.source_bytes,
85 "bytes_free": self.available_bytes,
86 "bytes_required": self.required_bytes,
87 "message": self.warning_message(source, target),
88 })
89 }
90}
91
92pub fn required_root_keyed_copy_free_bytes(source_bytes: u64) -> u64 {
93 source_bytes
94 .saturating_mul(ROOT_KEYED_COPY_DISK_FLOOR_NUMERATOR)
95 .saturating_add(ROOT_KEYED_COPY_DISK_FLOOR_DENOMINATOR - 1)
96 / ROOT_KEYED_COPY_DISK_FLOOR_DENOMINATOR
97}
98
99pub fn evaluate_root_keyed_copy_disk_floor(
100 source_bytes: u64,
101 available_bytes: u64,
102) -> DiskFloorDecision {
103 DiskFloorDecision {
104 source_bytes,
105 available_bytes,
106 required_bytes: required_root_keyed_copy_free_bytes(source_bytes),
107 }
108}
109
110pub fn available_disk_for(path: &Path) -> io::Result<u64> {
114 #[cfg(unix)]
115 {
116 use std::ffi::CString;
117 use std::os::unix::ffi::OsStrExt;
118
119 let probe = existing_ancestor(path);
120 let c_path = CString::new(probe.as_os_str().as_bytes())
121 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL byte"))?;
122 let mut stat = std::mem::MaybeUninit::<libc::statvfs>::uninit();
123 let result = unsafe { libc::statvfs(c_path.as_ptr(), stat.as_mut_ptr()) };
124 if result != 0 {
125 return Err(io::Error::last_os_error());
126 }
127 let stat = unsafe { stat.assume_init() };
128 Ok((stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64))
129 }
130
131 #[cfg(windows)]
132 {
133 let _ = path;
134 Ok(u64::MAX)
138 }
139}
140
141pub fn is_legacy_harness_partition_path(storage_root: &Path, candidate: &Path) -> bool {
144 let storage_root = lexical_normalize(storage_root);
145 let candidate = if candidate.is_absolute() {
146 lexical_normalize(candidate)
147 } else {
148 lexical_normalize(&storage_root.join(candidate))
149 };
150
151 let Ok(relative) = candidate.strip_prefix(&storage_root) else {
152 return false;
153 };
154
155 let mut components = relative.components();
156 let Some(Component::Normal(_harness)) = components.next() else {
157 return false;
158 };
159 let Some(Component::Normal(domain)) = components.next() else {
160 return false;
161 };
162
163 matches!(domain.to_str(), Some("callgraph" | "inspect"))
164}
165
166#[track_caller]
167pub fn debug_assert_not_legacy_harness_partition_path(storage_root: &Path, candidate: &Path) {
168 debug_assert!(
169 !is_legacy_harness_partition_path(storage_root, candidate),
170 "new-layout write path must not point into a legacy harness partition: {}",
171 candidate.display()
172 );
173}
174
175pub fn refuse_legacy_partition_write(
176 storage_root: &Path,
177 candidate: &Path,
178 operation: &str,
179) -> io::Result<()> {
180 if is_legacy_harness_partition_path(storage_root, candidate) {
181 return Err(io::Error::new(
182 io::ErrorKind::PermissionDenied,
183 format!(
184 "refusing {operation} into legacy harness partition {}",
185 candidate.display()
186 ),
187 ));
188 }
189 Ok(())
190}
191
192#[track_caller]
193pub fn guard_new_layout_write_path(
194 storage_root: &Path,
195 candidate: &Path,
196 operation: &str,
197) -> io::Result<()> {
198 debug_assert_not_legacy_harness_partition_path(storage_root, candidate);
199 refuse_legacy_partition_write(storage_root, candidate, operation)
200}
201
202pub fn inventory_legacy_partitions(
203 storage_root: &Path,
204) -> io::Result<Vec<LegacyPartitionInventoryEntry>> {
205 let storage_root = lexical_normalize(storage_root);
206 let boundary = match crate::walk_boundary::DeviceBoundary::for_root(&storage_root) {
207 Ok(boundary) => boundary,
208 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
211 Err(error) => return Err(error),
212 };
213 let mut entries = Vec::new();
214 for harness_entry in sorted_read_dir(&storage_root)? {
215 if !harness_entry.file_type()?.is_dir() {
216 continue;
217 }
218 let harness = harness_entry.file_name().to_string_lossy().to_string();
219 let harness_path = harness_entry.path();
220 if !boundary.should_descend(&harness_path)? {
223 crate::slog_warn!(
224 "legacy partition inventory skipped foreign filesystem mount {}",
225 harness_path.display()
226 );
227 continue;
228 }
229 let callgraph_path = harness_path.join(LegacyPartitionKind::Callgraph.as_str());
230 if callgraph_path.is_dir() && boundary.should_descend(&callgraph_path)? {
231 entries.extend(scan_legacy_callgraph_partitions(
232 &harness,
233 &callgraph_path,
234 &boundary,
235 )?);
236 }
237 let inspect_path = harness_path.join(LegacyPartitionKind::Inspect.as_str());
238 if inspect_path.is_dir() && boundary.should_descend(&inspect_path)? {
239 entries.extend(scan_legacy_inspect_partitions(
240 &harness,
241 &inspect_path,
242 &boundary,
243 )?);
244 }
245 }
246 entries.sort_by(|left, right| {
247 left.harness
248 .cmp(&right.harness)
249 .then_with(|| left.kind.as_str().cmp(right.kind.as_str()))
250 .then_with(|| left.key.cmp(&right.key))
251 });
252 Ok(entries)
253}
254
255pub fn summarize_legacy_partition_duplication(
256 storage_root: &Path,
257) -> io::Result<Vec<LegacyHarnessDuplication>> {
258 let mut summaries = BTreeMap::<String, LegacyHarnessDuplication>::new();
259 for entry in inventory_legacy_partitions(storage_root)? {
260 let summary =
261 summaries
262 .entry(entry.harness.clone())
263 .or_insert_with(|| LegacyHarnessDuplication {
264 harness: entry.harness.clone(),
265 partitions: 0,
266 bytes: 0,
267 });
268 summary.partitions += 1;
269 summary.bytes = summary.bytes.saturating_add(entry.bytes);
270 }
271 Ok(summaries.into_values().collect())
272}
273
274#[derive(Clone, Debug, Default)]
275struct PartitionAccumulator {
276 bytes: u64,
277 callgraph_pointer_mtime: Option<SystemTime>,
278 inspect_tier2_last_full_run: Option<i64>,
279}
280
281fn scan_legacy_callgraph_partitions(
282 harness: &str,
283 callgraph_dir: &Path,
284 boundary: &crate::walk_boundary::DeviceBoundary,
285) -> io::Result<Vec<LegacyPartitionInventoryEntry>> {
286 let mut partitions = BTreeMap::<String, PartitionAccumulator>::new();
287 for entry in sorted_read_dir(callgraph_dir)? {
288 let file_type = entry.file_type()?;
289 let name = entry.file_name().to_string_lossy().to_string();
290 if file_type.is_dir() {
291 if !looks_like_partition_key(&name) {
292 continue;
293 }
294 let partition = partitions.entry(name.clone()).or_default();
295 partition.bytes = partition
296 .bytes
297 .saturating_add(tree_size(&entry.path(), boundary)?);
298 if partition.callgraph_pointer_mtime.is_none() {
299 partition.callgraph_pointer_mtime = callgraph_pointer_mtime(callgraph_dir, &name);
300 }
301 continue;
302 }
303
304 let Some(key) = callgraph_partition_key_from_name(&name) else {
305 continue;
306 };
307 let partition = partitions.entry(key.clone()).or_default();
308 partition.bytes = partition.bytes.saturating_add(file_size(&entry.path())?);
309 if name.ends_with(".current") {
310 partition.callgraph_pointer_mtime =
311 entry.metadata().and_then(|meta| meta.modified()).ok();
312 }
313 }
314
315 Ok(partitions
316 .into_iter()
317 .map(|(key, partition)| LegacyPartitionInventoryEntry {
318 harness: harness.to_string(),
319 kind: LegacyPartitionKind::Callgraph,
320 path: callgraph_dir.join(&key),
321 key,
322 bytes: partition.bytes,
323 callgraph_pointer_mtime: partition.callgraph_pointer_mtime,
324 inspect_tier2_last_full_run: None,
325 })
326 .collect())
327}
328
329fn scan_legacy_inspect_partitions(
330 harness: &str,
331 inspect_dir: &Path,
332 boundary: &crate::walk_boundary::DeviceBoundary,
333) -> io::Result<Vec<LegacyPartitionInventoryEntry>> {
334 let mut partitions = BTreeMap::<String, PartitionAccumulator>::new();
335 for entry in sorted_read_dir(inspect_dir)? {
336 let file_type = entry.file_type()?;
337 let name = entry.file_name().to_string_lossy().to_string();
338 if file_type.is_dir() {
339 if !looks_like_partition_key(&name) {
340 continue;
341 }
342 let partition = partitions.entry(name.clone()).or_default();
343 partition.bytes = partition
344 .bytes
345 .saturating_add(tree_size(&entry.path(), boundary)?);
346 if partition.inspect_tier2_last_full_run.is_none() {
347 partition.inspect_tier2_last_full_run =
348 inspect_tier2_last_full_run(inspect_dir, &name);
349 }
350 continue;
351 }
352
353 let Some(key) = inspect_partition_key_from_name(&name) else {
354 continue;
355 };
356 let partition = partitions.entry(key.clone()).or_default();
357 partition.bytes = partition.bytes.saturating_add(file_size(&entry.path())?);
358 }
359
360 Ok(partitions
361 .into_iter()
362 .map(|(key, mut partition)| {
363 if partition.inspect_tier2_last_full_run.is_none() {
364 partition.inspect_tier2_last_full_run =
365 inspect_tier2_last_full_run(inspect_dir, &key);
366 }
367 LegacyPartitionInventoryEntry {
368 harness: harness.to_string(),
369 kind: LegacyPartitionKind::Inspect,
370 path: inspect_dir.join(&key),
371 key,
372 bytes: partition.bytes,
373 callgraph_pointer_mtime: None,
374 inspect_tier2_last_full_run: partition.inspect_tier2_last_full_run,
375 }
376 })
377 .collect())
378}
379
380fn callgraph_pointer_mtime(callgraph_dir: &Path, key: &str) -> Option<SystemTime> {
381 for candidate in [
382 callgraph_dir.join(format!("{key}.current")),
383 callgraph_dir.join(key).join(format!("{key}.current")),
384 ] {
385 if let Ok(modified) = fs::metadata(candidate).and_then(|metadata| metadata.modified()) {
386 return Some(modified);
387 }
388 }
389 None
390}
391
392fn inspect_tier2_last_full_run(inspect_dir: &Path, key: &str) -> Option<i64> {
393 let sqlite_path = [
394 inspect_dir.join(format!("{key}.sqlite")),
395 inspect_dir.join(key).join(format!("{key}.sqlite")),
396 ]
397 .into_iter()
398 .find(|candidate| candidate.is_file())?;
399
400 let conn = TrackedConnection::open_path_with_flags(
401 &sqlite_path,
402 OpenFlags::SQLITE_OPEN_READ_ONLY,
403 SqliteStore::InspectScopeCache,
404 )
405 .ok()?;
406 conn.busy_timeout(Duration::from_millis(500)).ok()?;
407 conn.query_row("SELECT MAX(last_full_run) FROM tier2_meta", [], |row| {
408 row.get::<_, Option<i64>>(0)
409 })
410 .ok()
411 .flatten()
412}
413
414fn callgraph_partition_key_from_name(name: &str) -> Option<String> {
415 if name.contains(".tmp.") {
416 return None;
417 }
418 if let Some(key) = name.strip_suffix(".current") {
419 return looks_like_partition_key(key).then(|| key.to_string());
420 }
421 let base = sqliteish_base_name(name)?;
422 let key = if let Some((candidate, generation)) = base.split_once(".g") {
423 if generation.is_empty() {
424 return None;
425 }
426 candidate
427 } else {
428 base
429 };
430 looks_like_partition_key(key).then(|| key.to_string())
431}
432
433fn inspect_partition_key_from_name(name: &str) -> Option<String> {
434 if name.contains(".tmp.") {
435 return None;
436 }
437 let base = sqliteish_base_name(name)?;
438 looks_like_partition_key(base).then(|| base.to_string())
439}
440
441fn sqliteish_base_name(name: &str) -> Option<&str> {
442 SQLITE_SUFFIXES
443 .iter()
444 .find_map(|suffix| name.strip_suffix(suffix))
445}
446
447fn looks_like_partition_key(value: &str) -> bool {
448 value.len() == 16 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
449}
450
451fn file_size(path: &Path) -> io::Result<u64> {
452 Ok(fs::metadata(path)?.len())
453}
454
455fn tree_size(path: &Path, boundary: &crate::walk_boundary::DeviceBoundary) -> io::Result<u64> {
456 if !path.exists() {
457 return Ok(0);
458 }
459 let metadata = fs::metadata(path)?;
460 if metadata.is_file() {
461 return Ok(metadata.len());
462 }
463 if !metadata.is_dir() {
464 return Ok(0);
465 }
466 if !boundary.should_descend(path)? {
467 crate::slog_warn!(
468 "legacy partition inventory skipped foreign filesystem mount {}",
469 path.display()
470 );
471 return Ok(0);
472 }
473
474 let mut total = 0_u64;
475 for entry in fs::read_dir(path)? {
476 total = total.saturating_add(tree_size(&entry?.path(), boundary)?);
477 }
478 Ok(total)
479}
480
481fn sorted_read_dir(path: &Path) -> io::Result<Vec<fs::DirEntry>> {
482 let entries = match fs::read_dir(path) {
483 Ok(entries) => entries,
484 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
485 Err(error) => return Err(error),
486 };
487 let mut entries = entries.collect::<io::Result<Vec<_>>>()?;
488 entries.sort_by_key(|entry| entry.file_name());
489 Ok(entries)
490}
491
492fn lexical_normalize(path: &Path) -> PathBuf {
493 let mut normalized = PathBuf::new();
494 for component in path.components() {
495 match component {
496 Component::ParentDir => {
497 if !normalized.pop() {
498 normalized.push(component);
499 }
500 }
501 Component::CurDir => {}
502 other => normalized.push(other.as_os_str()),
503 }
504 }
505 normalized
506}
507
508#[cfg(unix)]
509fn existing_ancestor(path: &Path) -> &Path {
510 let mut current = path;
511 while !current.exists() {
512 if let Some(parent) = current.parent() {
513 current = parent;
514 } else {
515 break;
516 }
517 }
518 current
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use filetime::FileTime;
525 use rusqlite::params;
526 use std::panic::catch_unwind;
527 use std::time::UNIX_EPOCH;
528 use tempfile::TempDir;
529
530 #[test]
531 fn legacy_partition_guard_matches_exact_domains() {
532 let storage_root = PathBuf::from("/tmp/aft-storage");
533 assert!(is_legacy_harness_partition_path(
534 &storage_root,
535 &storage_root.join("opencode/callgraph/0123456789abcdef.current")
536 ));
537 assert!(is_legacy_harness_partition_path(
538 &storage_root,
539 &storage_root.join("pi/inspect/0123456789abcdef.sqlite")
540 ));
541 assert!(!is_legacy_harness_partition_path(
542 &storage_root,
543 &storage_root.join("opencode/callgraph-old/0123456789abcdef.sqlite")
544 ));
545 assert!(!is_legacy_harness_partition_path(
546 &storage_root,
547 &storage_root.join("index/0123456789abcdef.sqlite")
548 ));
549 assert!(!is_legacy_harness_partition_path(
550 &storage_root,
551 Path::new("/elsewhere/opencode/callgraph/0123456789abcdef.sqlite")
552 ));
553 }
554
555 #[test]
556 fn debug_assert_and_refusal_cover_legacy_write_paths() {
557 let storage_root = PathBuf::from("/tmp/aft-storage");
558 let legacy_target = storage_root.join("opencode/callgraph/0123456789abcdef.sqlite");
559 let panic = catch_unwind(|| {
560 debug_assert_not_legacy_harness_partition_path(&storage_root, &legacy_target);
561 });
562 assert!(panic.is_err());
563
564 let error = refuse_legacy_partition_write(&storage_root, &legacy_target, "publish")
565 .expect_err("legacy write must be refused");
566 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
567 assert!(error
568 .to_string()
569 .contains("refusing publish into legacy harness partition"));
570 }
571
572 #[test]
573 fn root_keyed_copy_disk_floor_boundaries() {
574 let exact = evaluate_root_keyed_copy_disk_floor(20, 30);
575 assert_eq!(exact.required_bytes, 30);
576 assert!(exact.allows_copy());
577 assert!(!exact.should_skip_copy());
578
579 let below = evaluate_root_keyed_copy_disk_floor(20, 29);
580 assert!(below.should_skip_copy());
581 assert!(below
582 .warning_message(Path::new("/legacy"), Path::new("/shared"))
583 .contains("1.5× floor"));
584
585 let above = evaluate_root_keyed_copy_disk_floor(20, 31);
586 assert!(above.allows_copy());
587 assert_eq!(
588 below.configure_warning(Path::new("/legacy"), Path::new("/shared"))["kind"],
589 "root_keyed_disk_floor"
590 );
591 }
592
593 #[test]
594 fn inventory_fixture_reports_partition_sizes_and_freshness() {
595 let fixture = write_legacy_inventory_fixture();
596 let storage_root = fixture.temp.path();
597
598 let inventory = inventory_legacy_partitions(storage_root).expect("inventory");
599 assert_eq!(inventory.len(), 2);
600
601 let callgraph = inventory
602 .iter()
603 .find(|entry| entry.kind == LegacyPartitionKind::Callgraph)
604 .expect("callgraph entry");
605 let expected_callgraph_bytes = partition_bytes_on_disk(
606 &storage_root.join("opencode/callgraph"),
607 "0123456789abcdef",
608 callgraph_partition_key_from_name,
609 );
610 assert_eq!(callgraph.harness, "opencode");
611 assert_eq!(callgraph.key, "0123456789abcdef");
612 assert_eq!(
613 callgraph.path,
614 storage_root.join("opencode/callgraph/0123456789abcdef")
615 );
616 assert_eq!(callgraph.bytes, expected_callgraph_bytes);
617 let pointer_secs = callgraph
618 .callgraph_pointer_mtime
619 .expect("pointer mtime")
620 .duration_since(UNIX_EPOCH)
621 .expect("mtime after epoch")
622 .as_secs();
623 assert_eq!(pointer_secs, 1_750_000_000);
624 assert_eq!(callgraph.inspect_tier2_last_full_run, None);
625
626 let inspect = inventory
627 .iter()
628 .find(|entry| entry.kind == LegacyPartitionKind::Inspect)
629 .expect("inspect entry");
630 let minimum_inspect_bytes =
631 file_size(&storage_root.join("pi/inspect/fedcba9876543210.sqlite"))
632 .expect("inspect sqlite size");
633 assert_eq!(inspect.harness, "pi");
634 assert_eq!(inspect.key, "fedcba9876543210");
635 assert_eq!(
636 inspect.path,
637 storage_root.join("pi/inspect/fedcba9876543210")
638 );
639 assert!(inspect.bytes >= minimum_inspect_bytes);
640 assert_eq!(inspect.callgraph_pointer_mtime, None);
641 assert_eq!(inspect.inspect_tier2_last_full_run, Some(250));
642
643 let summary = summarize_legacy_partition_duplication(storage_root).expect("summary");
644 assert_eq!(
645 summary,
646 vec![
647 LegacyHarnessDuplication {
648 harness: "opencode".to_string(),
649 partitions: 1,
650 bytes: expected_callgraph_bytes,
651 },
652 LegacyHarnessDuplication {
653 harness: "pi".to_string(),
654 partitions: 1,
655 bytes: inspect.bytes,
656 },
657 ]
658 );
659 }
660
661 struct LegacyInventoryFixture {
662 temp: TempDir,
663 }
664
665 fn write_legacy_inventory_fixture() -> LegacyInventoryFixture {
666 let temp = tempfile::tempdir().expect("tempdir");
667
668 let callgraph_dir = temp.path().join("opencode/callgraph");
669 fs::create_dir_all(&callgraph_dir).expect("create callgraph dir");
670 fs::write(
671 callgraph_dir.join("0123456789abcdef.current"),
672 b"0123456789abcdef.g1.1.sqlite\n",
673 )
674 .expect("write pointer");
675 fs::write(
676 callgraph_dir.join("0123456789abcdef.g1.1.sqlite"),
677 b"callgraph-db",
678 )
679 .expect("write generation db");
680 fs::write(
681 callgraph_dir.join("0123456789abcdef.g1.1.sqlite-wal"),
682 b"wal",
683 )
684 .expect("write generation wal");
685 fs::write(
686 callgraph_dir.join("0123456789abcdef.current.tmp.123"),
687 b"ignored-temp",
688 )
689 .expect("write ignored temp");
690 filetime::set_file_mtime(
691 callgraph_dir.join("0123456789abcdef.current"),
692 FileTime::from_unix_time(1_750_000_000, 0),
693 )
694 .expect("set pointer mtime");
695
696 let inspect_dir = temp.path().join("pi/inspect");
697 fs::create_dir_all(&inspect_dir).expect("create inspect dir");
698 let sqlite_path = inspect_dir.join("fedcba9876543210.sqlite");
699 let conn = Connection::open(&sqlite_path).expect("open inspect db");
700 conn.execute(
701 "CREATE TABLE tier2_meta (category TEXT NOT NULL, project_key TEXT NOT NULL, last_full_run INTEGER NOT NULL)",
702 [],
703 )
704 .expect("create tier2_meta");
705 conn.execute(
706 "INSERT INTO tier2_meta (category, project_key, last_full_run) VALUES (?1, ?2, ?3)",
707 params!["dead_code", "fedcba9876543210", 100_i64],
708 )
709 .expect("insert first tier2 row");
710 conn.execute(
711 "INSERT INTO tier2_meta (category, project_key, last_full_run) VALUES (?1, ?2, ?3)",
712 params!["duplicates", "fedcba9876543210", 250_i64],
713 )
714 .expect("insert second tier2 row");
715 drop(conn);
716 fs::write(inspect_dir.join("misc.txt"), b"ignored").expect("write ignored file");
717
718 LegacyInventoryFixture { temp }
719 }
720
721 fn partition_bytes_on_disk(
722 domain_dir: &Path,
723 expected_key: &str,
724 key_fn: fn(&str) -> Option<String>,
725 ) -> u64 {
726 sorted_read_dir(domain_dir)
727 .expect("read partition dir")
728 .into_iter()
729 .filter_map(|entry| {
730 let name = entry.file_name().to_string_lossy().to_string();
731 let key = key_fn(&name)?;
732 if key != expected_key {
733 return None;
734 }
735 Some(file_size(&entry.path()).expect("partition file size"))
736 })
737 .sum()
738 }
739}