use std::path::{Path, PathBuf};
use super::*;
fn survey(value: serde_json::Value) -> Survey {
serde_json::from_value(value).expect("the fixture must match the survey schema")
}
fn no_names() -> SessionNames {
SessionNames::new()
}
fn session_row(id: &str, name: &str) -> DirectoryRow {
DirectoryRow {
id: id.to_string(),
name: name.to_string(),
}
}
fn two_sessions() -> Survey {
survey(json!({
"generated_at": "2026-09-10T00:00:00Z",
"partial": false,
"by_session": [
{
"session_id": "trusty-tools-95",
"bytes": 30_000_000_000u64,
"build_dir_bytes": 24_000_000_000u64,
"worktree_count": 3,
"tiers": { "stale": 1, "review": 1, "keep": 1, "missing": 0 },
"worktree_paths": []
},
{
"session_id": "trusty-tools-12",
"bytes": 10_000_000_000u64,
"build_dir_bytes": 9_000_000_000u64,
"worktree_count": 1,
"tiers": { "stale": 0, "review": 0, "keep": 1, "missing": 0 },
"worktree_paths": []
},
{
"session_id": null,
"bytes": 5_000_000_000u64,
"build_dir_bytes": 4_000_000_000u64,
"worktree_count": 2,
"tiers": { "stale": 2, "review": 0, "keep": 0, "missing": 0 },
"worktree_paths": []
}
],
"root": { "projects": [] }
}))
}
fn one_session_across_two_projects() -> Survey {
survey(json!({
"generated_at": "2026-09-10T00:00:00Z",
"partial": false,
"by_session": [],
"root": { "projects": [
{
"name": "bobmatnyc/trusty-tools",
"path": "/w/bobmatnyc/trusty-tools",
"bytes": 40_000_000_000u64,
"worktrees": [
{
"path": "/w/bobmatnyc/trusty-tools/.claude/worktrees/a",
"tier": "stale",
"bytes": 20_000_000_000u64,
"build_dir_bytes": 18_000_000_000u64,
"owning_session": "trusty-tools-95"
},
{
"path": "/w/bobmatnyc/trusty-tools/.claude/worktrees/b",
"tier": "keep",
"bytes": 1_000_000_000u64,
"build_dir_bytes": 0,
"owning_session": "trusty-tools-12"
}
]
},
{
"name": "bobmatnyc/other",
"path": "/w/bobmatnyc/other",
"bytes": 5_000_000_000u64,
"worktrees": [
{
"path": "/w/bobmatnyc/other/.claude/worktrees/c",
"tier": "review",
"bytes": 30_000_000_000u64,
"build_dir_bytes": 25_000_000_000u64,
"owning_session": "trusty-tools-95"
},
{
"path": "/w/bobmatnyc/other/.claude/worktrees/d",
"tier": "missing",
"bytes": null,
"build_dir_bytes": null,
"owning_session": "trusty-tools-95"
}
]
}
] }
}))
}
#[test]
fn sessions_report_sorts_by_bytes_descending() {
let DiskReport::Sessions { sessions, .. } = sessions_report(&two_sessions(), None, &no_names())
else {
panic!("the no-argument view must be the sessions view");
};
let ids: Vec<Option<&str>> = sessions
.iter()
.map(|r| r.session_id.as_deref())
.collect::<Vec<_>>();
assert_eq!(
ids,
vec![Some("trusty-tools-95"), Some("trusty-tools-12"), None],
"bytes descending, with the unattributed bucket last"
);
let bytes: Vec<u64> = sessions.iter().map(|r| r.bytes).collect();
assert!(
bytes.windows(2).all(|w| w[0] >= w[1]),
"each row must be at least as large as the next: {bytes:?}"
);
}
#[test]
fn sessions_report_totals_every_row() {
let DiskReport::Sessions {
sessions, total, ..
} = sessions_report(
&two_sessions(),
Some("bobmatnyc/trusty-tools".to_string()),
&no_names(),
)
else {
panic!("the no-argument view must be the sessions view");
};
assert_eq!(total.bytes, 45_000_000_000);
assert_eq!(total.build_dir_bytes, 37_000_000_000);
assert_eq!(
total.source_bytes, 8_000_000_000,
"source is what is left after the build directories"
);
assert_eq!(total.worktree_count, 6);
assert_eq!(sessions[0].source_bytes, 6_000_000_000);
}
#[test]
fn a_daemon_without_the_rollup_reports_nothing() {
let older = survey(json!({
"generated_at": "2026-09-10T00:00:00Z",
"partial": false,
"root": { "projects": [] }
}));
let DiskReport::Sessions {
sessions, total, ..
} = sessions_report(&older, None, &no_names())
else {
panic!("the no-argument view must be the sessions view");
};
assert!(sessions.is_empty());
assert_eq!(total.bytes, 0);
}
#[test]
fn a_session_report_splits_build_bytes_from_the_rest() {
let report = session_report(
&one_session_across_two_projects(),
"trusty-tools-95",
&no_names(),
)
.expect("the session owns worktrees");
let DiskReport::Session { classes, total, .. } = report else {
panic!("an argument must produce the session view");
};
assert_eq!(total.bytes, 50_000_000_000);
assert_eq!(total.build_dir_bytes, 43_000_000_000);
assert_eq!(total.source_bytes, 7_000_000_000);
assert_eq!(classes[0].class, "build");
assert_eq!(classes[0].bytes, 43_000_000_000);
assert_eq!(classes[1].class, "source");
assert_eq!(classes[1].bytes, 7_000_000_000);
}
#[test]
fn a_session_report_spans_projects() {
let report = session_report(
&one_session_across_two_projects(),
"trusty-tools-95",
&no_names(),
)
.expect("the session owns worktrees");
let DiskReport::Session { worktrees, .. } = report else {
panic!("an argument must produce the session view");
};
let projects: Vec<&str> = worktrees.iter().map(|w| w.project.as_str()).collect();
assert!(
projects.contains(&"bobmatnyc/trusty-tools") && projects.contains(&"bobmatnyc/other"),
"both projects must appear: {projects:?}"
);
assert_eq!(
worktrees[0].path,
PathBuf::from("/w/bobmatnyc/other/.claude/worktrees/c"),
"the largest worktree leads, whichever project it is under"
);
let bytes: Vec<u64> = worktrees.iter().map(|w| w.bytes.unwrap_or(0)).collect();
assert!(
bytes.windows(2).all(|w| w[0] >= w[1]),
"bytes descending: {bytes:?}"
);
}
#[test]
fn an_unmeasured_worktree_is_listed_and_adds_nothing() {
let report = session_report(
&one_session_across_two_projects(),
"trusty-tools-95",
&no_names(),
)
.expect("the session owns worktrees");
let DiskReport::Session {
worktrees, total, ..
} = report
else {
panic!("an argument must produce the session view");
};
let unmeasured = worktrees
.iter()
.find(|w| w.path.ends_with("d"))
.expect("the unmeasured worktree must still be listed");
assert_eq!(unmeasured.bytes, None);
assert_eq!(unmeasured.source_bytes, None);
assert_eq!(
total.worktree_count, 3,
"it counts as a worktree even though it adds no bytes"
);
assert_eq!(total.bytes, 50_000_000_000);
}
#[test]
fn an_unknown_session_is_an_error() {
let err = session_report(
&one_session_across_two_projects(),
"no-such-session",
&no_names(),
)
.expect_err("an unattributed id must not produce a report");
let message = err.to_string();
assert!(message.contains("no-such-session"), "{message}");
assert!(
message.contains("tm session disk"),
"the error must name the command that lists what IS attributed: {message}"
);
}
#[test]
fn a_jsonrpc_error_is_reported_not_swallowed() {
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32603, "message": "daemon is shutting down" }
});
let err = survey_from_rpc(&body).expect_err("a JSON-RPC error must not parse as a survey");
assert!(err.to_string().contains("daemon is shutting down"), "{err}");
}
#[test]
fn a_tool_error_is_reported_not_swallowed() {
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{
"type": "text",
"text": "disk_survey: unknown `group_by` value `project`"
}],
"isError": true
}
});
let err = survey_from_rpc(&body).expect_err("an isError result must not parse as a survey");
assert!(
err.to_string().contains("unknown `group_by` value"),
"{err}"
);
}
#[test]
fn a_survey_payload_round_trips() {
let payload = json!({
"generated_at": "2026-09-10T00:00:00Z",
"partial": true,
"by_session": [],
"root": { "projects": [] }
});
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{ "type": "text", "text": payload.to_string() }],
"isError": false
}
});
let survey = survey_from_rpc(&body).expect("a well-formed result must parse");
assert_eq!(survey.generated_at, "2026-09-10T00:00:00Z");
assert!(survey.partial);
}
#[test]
fn a_worktree_cwd_still_names_its_project() {
let root = tempfile::tempdir().expect("temp workspace root");
let cwd = root
.path()
.join("bobmatnyc")
.join("trusty-tools")
.join(".claude")
.join("worktrees")
.join("agent-a82c1d86143c79d5f");
std::fs::create_dir_all(&cwd).expect("create the worktree fixture");
assert_eq!(
project_filter_within(&cwd, root.path()),
Some("bobmatnyc/trusty-tools".to_string())
);
assert_eq!(
project_filter_within(
&root.path().join("bobmatnyc").join("trusty-tools"),
root.path()
),
Some("bobmatnyc/trusty-tools".to_string())
);
}
#[test]
fn a_cwd_outside_the_workspace_root_has_no_filter() {
let root = tempfile::tempdir().expect("temp workspace root");
let elsewhere = tempfile::tempdir().expect("temp cwd");
assert_eq!(project_filter_within(elsewhere.path(), root.path()), None);
assert_eq!(project_filter_within(root.path(), root.path()), None);
assert_eq!(
project_filter_within(&root.path().join("bobmatnyc"), root.path()),
None
);
}
#[test]
fn the_listing_renders_a_total_line() {
let report = sessions_report(
&two_sessions(),
Some("bobmatnyc/trusty-tools".to_string()),
&no_names(),
);
let text = render(&report);
assert!(text.contains("project bobmatnyc/trusty-tools"), "{text}");
assert!(text.contains("trusty-tools-95"), "{text}");
assert!(
text.contains("(unattributed)"),
"the null bucket needs a label an operator can read: {text}"
);
let total = text
.lines()
.find(|l| l.starts_with("TOTAL"))
.expect("a total line");
assert!(
total.contains("41.9 GiB"),
"the total renders through the shared byte formatter: {total}"
);
}
#[test]
fn a_uuid_session_id_keeps_the_columns_aligned() {
let uuid = "0b318c84-bae9-4a50-8832-65ed61f8ab22";
assert_eq!(uuid.len(), 36, "the id width this test exists for");
let wide = survey(json!({
"generated_at": "2026-09-10T00:00:00Z",
"partial": false,
"by_session": [{
"session_id": uuid,
"bytes": 1_000_000_000u64,
"build_dir_bytes": 400_000_000u64,
"worktree_count": 1,
"tiers": { "stale": 0, "review": 0, "keep": 1, "missing": 0 },
"worktree_paths": []
}],
"root": { "projects": [] }
}));
let text = render(&sessions_report(&wide, None, &no_names()));
let lines: Vec<&str> = text.lines().collect();
let header = lines
.iter()
.find(|l| l.starts_with("SESSION"))
.expect("a header line");
let row = lines
.iter()
.find(|l| l.starts_with(uuid))
.expect("the session's own row");
let total = lines
.iter()
.find(|l| l.starts_with("TOTAL"))
.expect("a total line");
assert_eq!(
header.len(),
row.len(),
"a UUID must not push the row wider than the header:\n{header}\n{row}"
);
assert_eq!(
header.len(),
total.len(),
"and the total must line up with both:\n{header}\n{total}"
);
}
#[test]
fn the_breakdown_names_every_class() {
let report = session_report(
&one_session_across_two_projects(),
"trusty-tools-95",
&no_names(),
)
.expect("the session owns worktrees");
let text = render(&report);
assert!(text.contains("session trusty-tools-95"), "{text}");
assert!(text.contains("build"), "{text}");
assert!(text.contains("source"), "{text}");
assert!(
text.contains("/w/bobmatnyc/other/.claude/worktrees/d"),
"an unmeasured worktree is still listed: {text}"
);
assert!(
text.contains('?'),
"and its figures read as unmeasured, not as zero: {text}"
);
}
#[test]
fn a_relative_cwd_has_no_filter() {
assert_eq!(
project_filter_within(Path::new("bobmatnyc/trusty-tools"), Path::new("/w")),
None
);
}
#[test]
fn a_friendly_name_resolves_to_its_session_uuid() {
let uuid = "0b318c84-bae9-4a50-8832-65ed61f8ab22";
let directory = vec![
session_row(uuid, "trusty-tools-95"),
session_row("11111111-2222-3333-4444-555555555555", "trusty-tools-12"),
];
assert_eq!(
resolve_session_id(&directory, "trusty-tools-95").expect("the name names a session"),
uuid
);
assert_eq!(
resolve_session_id(&directory, uuid).expect("an id resolves to itself"),
uuid
);
assert_eq!(
resolve_session_id(&directory, "trusty-tools-9").expect("an unambiguous prefix resolves"),
uuid
);
}
#[test]
fn an_ended_sessions_uuid_resolves_without_the_daemon_listing_it() {
let gone = "0b318c84-bae9-4a50-8832-65ed61f8ab22";
assert_eq!(
resolve_session_id(&[], gone).expect("a UUID passes through an empty directory"),
gone
);
}
#[test]
fn an_unknown_name_is_a_distinct_error() {
let directory = vec![session_row(
"0b318c84-bae9-4a50-8832-65ed61f8ab22",
"trusty-tools-95",
)];
let unknown = resolve_session_id(&directory, "no-such-session")
.expect_err("a name matching nothing must not resolve");
assert!(
unknown.to_string().contains("no session named"),
"the unresolvable-name arm must say so: {unknown}"
);
assert!(unknown.to_string().contains("no-such-session"), "{unknown}");
let empty = session_report(
&one_session_across_two_projects(),
"0b318c84-bae9-4a50-8832-65ed61f8ab22",
&no_names(),
)
.expect_err("a session owning no worktree must not produce a report");
assert!(
empty.to_string().contains("holds no worktrees"),
"the holds-nothing arm must say so instead: {empty}"
);
assert!(
!empty.to_string().contains("no session named"),
"the two arms must not share a message: {empty}"
);
}
#[test]
fn the_listing_renders_the_friendly_name_when_one_is_known() {
let uuid = "0b318c84-bae9-4a50-8832-65ed61f8ab22";
let names = session_names(&[session_row(uuid, "trusty-tools-95")]);
let report = sessions_report(&one_attributed_session(uuid), None, &names);
let DiskReport::Sessions { ref sessions, .. } = report else {
panic!("the no-argument view must be the sessions view");
};
assert_eq!(
sessions[0].session_id.as_deref(),
Some(uuid),
"`--json` keys on the UUID, which a name cannot replace"
);
assert_eq!(sessions[0].session_name.as_deref(), Some("trusty-tools-95"));
let text = render(&report);
assert!(text.contains("trusty-tools-95"), "{text}");
assert!(
!text.contains(uuid),
"the table renders the name in the UUID's place: {text}"
);
}
#[test]
fn a_long_session_name_is_truncated_to_keep_the_columns_aligned() {
let uuid = "0b318c84-bae9-4a50-8832-65ed61f8ab22";
let long = "trusty-tools-a-very-long-friendly-session-name-xyz";
assert_eq!(long.chars().count(), 50, "the width this test exists for");
let names = session_names(&[session_row(uuid, long)]);
let text = render(&sessions_report(
&one_attributed_session(uuid),
None,
&names,
));
let lines: Vec<&str> = text.lines().collect();
let header = lines
.iter()
.find(|l| l.starts_with("SESSION"))
.expect("a header line");
let row = lines
.iter()
.find(|l| l.starts_with("trusty-tools-a-very-long"))
.expect("the session's own row");
let total = lines
.iter()
.find(|l| l.starts_with("TOTAL"))
.expect("a total line");
assert_eq!(
header.chars().count(),
row.chars().count(),
"a long name must not push the row wider than the header:\n{header}\n{row}"
);
assert_eq!(header.chars().count(), total.chars().count());
assert!(
row.contains('…'),
"and the cut must be visible rather than silent: {row}"
);
}
fn one_attributed_session(uuid: &str) -> Survey {
survey(json!({
"generated_at": "2026-09-10T00:00:00Z",
"partial": false,
"by_session": [{
"session_id": uuid,
"bytes": 1_000_000_000u64,
"build_dir_bytes": 400_000_000u64,
"worktree_count": 1,
"tiers": { "stale": 0, "review": 0, "keep": 1, "missing": 0 },
"worktree_paths": []
}],
"root": { "projects": [] }
}))
}