use std::path::{Path, PathBuf};
use crate::models::{ContentBlock, Message};
use crate::session_manager::{
SavedSession, SessionListFilter, SessionManager, SessionMutator,
create_saved_session_with_id_and_mode,
};
use crate::session_projection::{SessionQuery, SessionSortMode, project_sessions};
use crate::session_resume::{AutoResumeDecision, ResumeRequest, decide_auto_resume};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Contract {
PersistentSessions,
ControlPlane,
Shared,
}
#[derive(Debug, Clone, Copy)]
pub struct AcceptanceCase {
pub contract: Contract,
pub behavior: &'static str,
pub test: &'static str,
}
pub const ACCEPTANCE_MATRIX: &[AcceptanceCase] = &[
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Sessions persist across a restart and are found again by a fresh manager",
test: "sessions_persist_across_restart",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Browsing is scoped to the current workspace; another project's sessions are not listed",
test: "browsing_is_scoped_to_the_selected_workspace",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Auto-resume with a valid session reattaches to it",
test: "auto_resume_valid_session",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Auto-resume with no session for this workspace starts fresh with a receipt",
test: "auto_resume_missing_session",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Auto-resume with a corrupt session file starts fresh with a receipt",
test: "auto_resume_corrupt_session",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Auto-resume disabled (the default) never reattaches and says nothing",
test: "auto_resume_disabled_by_default",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Auto-resume never resumes a session recorded against a different workspace",
test: "auto_resume_never_crosses_a_workspace",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Search, sort, and preview inputs come from one projection shared by every surface",
test: "search_sort_and_preview_are_one_projection",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Rename persists to disk and survives a reload",
test: "rename_persists_and_survives_reload",
},
AcceptanceCase {
contract: Contract::ControlPlane,
behavior: "Archive is durable, hides the session from default listings, and is reversible",
test: "archive_is_durable_reversible_and_hidden_by_default",
},
AcceptanceCase {
contract: Contract::ControlPlane,
behavior: "The real picker's filtered/sorted view equals the API projection of the same query",
test: "tui_and_api_listings_agree",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "One workspace matcher: a nested path resolves to its repository root's scope",
test: "a_nested_path_resolves_to_the_same_scope_as_its_repository_root",
},
AcceptanceCase {
contract: Contract::ControlPlane,
behavior: "An archive applied to the active session survives the next autosave",
test: "archive_survives_the_next_autosave",
},
AcceptanceCase {
contract: Contract::ControlPlane,
behavior: "A rename applied to the active session survives the next autosave",
test: "rename_survives_the_next_autosave",
},
AcceptanceCase {
contract: Contract::ControlPlane,
behavior: "An external writer is refused with a typed conflict while a session is live",
test: "an_external_writer_is_refused_while_a_session_is_live",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Auto-resume skips corrupt newer candidates and names the skipped count",
test: "auto_resume_skips_a_corrupt_newest_and_reports_how_many",
},
AcceptanceCase {
contract: Contract::PersistentSessions,
behavior: "Auto-resume reports honestly when every candidate is unreadable",
test: "auto_resume_reports_when_every_candidate_is_unreadable",
},
AcceptanceCase {
contract: Contract::Shared,
behavior: "The auto-resume candidate walk is bounded",
test: "auto_resume_candidate_walk_is_bounded",
},
AcceptanceCase {
contract: Contract::ControlPlane,
behavior: "Session archive filters resolve the include_archived/archived_only pair like threads",
test: "session_and_thread_archive_filters_share_one_resolution",
},
AcceptanceCase {
contract: Contract::Shared,
behavior: "Browsing and resume decisions perform no provider or network call",
test: "browsing_and_resume_are_offline",
},
AcceptanceCase {
contract: Contract::Shared,
behavior: "History and search results are bounded, never unbounded reads",
test: "history_and_search_results_are_bounded",
},
AcceptanceCase {
contract: Contract::Shared,
behavior: "Projections report only recorded state — no fabricated live status",
test: "projections_never_fabricate_live_state",
},
];
pub const HUMAN_VERIFICATION: &[&str] = &[
"Sessions rail legibility and truncation at narrow widths (40/60/80 columns) and short heights",
"Rail row, archived label, and current-session marker contrast in each shipped theme",
"Keyboard/modal ownership: `e`/`x` inside the picker do not leak to the composer, and the rail's row activation does not steal focus mid-turn",
"Localized rail and archive strings render without clipping in ja / zh-Hans / ko / vi / pt-BR / es-419",
"Web dashboard saved-sessions section: layout at mobile widths, and resume-into-thread behaviour against a live runtime",
"SSE gap/reconnect state in the dashboard after resuming a session into a new thread",
"Approval and user-input targeting from the dashboard against a live pending approval",
];
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
struct Fixture {
dir: TempDir,
workspace: PathBuf,
manager: SessionManager,
}
impl Fixture {
fn new() -> Self {
let dir = TempDir::new().expect("tempdir");
let workspace = dir.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace");
let manager =
SessionManager::new(dir.path().join("sessions")).expect("session manager");
Self {
dir,
workspace,
manager,
}
}
fn reopen(&self) -> SessionManager {
SessionManager::new(self.manager.sessions_dir().to_path_buf())
.expect("reopen session manager")
}
fn save(&self, id: &str, title: &str, workspace: &Path) {
let session = saved(id, title, workspace);
self.manager.save_session(&session).expect("save session");
}
}
fn metadata_row(
id: &str,
title: &str,
workspace: &str,
) -> crate::session_manager::SessionMetadata {
let mut session = saved(id, title, Path::new(workspace));
session.metadata.workspace = PathBuf::from(workspace);
session.metadata
}
fn saved(id: &str, title: &str, workspace: &Path) -> SavedSession {
let messages = vec![
Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: format!("prompt for {title}"),
cache_control: None,
}],
},
Message {
role: "assistant".to_string(),
content: vec![ContentBlock::Text {
text: format!("reply for {title}"),
cache_control: None,
}],
},
];
let mut session = create_saved_session_with_id_and_mode(
id.to_string(),
&messages,
"deepseek-chat",
workspace,
42,
None,
Some("agent"),
);
session.metadata.title = title.to_string();
session
}
#[test]
fn every_matrix_row_names_a_real_test() {
let source = include_str!("session_control_acceptance.rs");
for case in ACCEPTANCE_MATRIX {
assert!(
source.contains(&format!("fn {}(", case.test)),
"matrix row `{}` names missing test `{}`",
case.behavior,
case.test
);
}
assert!(
!HUMAN_VERIFICATION.is_empty(),
"the human-verification list must stay explicit, not silently empty"
);
}
#[test]
fn matrix_covers_both_issues_and_their_shared_constraints() {
for contract in [
Contract::PersistentSessions,
Contract::ControlPlane,
Contract::Shared,
] {
assert!(
ACCEPTANCE_MATRIX
.iter()
.any(|case| case.contract == contract),
"no acceptance rows for {contract:?}"
);
}
}
#[test]
fn sessions_persist_across_restart() {
let fx = Fixture::new();
fx.save("persisted", "Whale migration", &fx.workspace);
let reopened = fx.reopen();
let listed = reopened.list_sessions().expect("list");
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].title, "Whale migration");
let loaded = reopened.load_session("persisted").expect("load");
assert_eq!(loaded.messages.len(), 2);
}
#[test]
fn browsing_is_scoped_to_the_selected_workspace() {
let fx = Fixture::new();
let other = fx.dir.path().join("other");
std::fs::create_dir_all(&other).expect("other workspace");
fx.save("mine", "Mine", &fx.workspace);
fx.save("theirs", "Theirs", &other);
let all = fx.manager.list_sessions().expect("list");
let scoped = project_sessions(
&all,
&SessionQuery::default().scoped_to(&fx.workspace),
None,
);
assert_eq!(
scoped.iter().map(|s| s.id.as_str()).collect::<Vec<_>>(),
vec!["mine"]
);
}
#[test]
fn auto_resume_valid_session() {
let fx = Fixture::new();
fx.save("valid", "Resumable work", &fx.workspace);
let decision =
decide_auto_resume(true, &ResumeRequest::default(), &fx.workspace, &fx.manager);
assert_eq!(decision.session_id(), Some("valid"));
assert!(!decision.starts_fresh());
}
#[test]
fn auto_resume_missing_session() {
let fx = Fixture::new();
let decision =
decide_auto_resume(true, &ResumeRequest::default(), &fx.workspace, &fx.manager);
assert_eq!(decision, AutoResumeDecision::NoSession);
assert!(
decision.status_message().is_some(),
"fallback needs a receipt"
);
}
#[test]
fn auto_resume_corrupt_session() {
let fx = Fixture::new();
fx.save("corrupt", "Half-written", &fx.workspace);
let path = fx.manager.sessions_dir().join("corrupt.json");
let content = std::fs::read_to_string(&path).expect("read");
let truncated = content
.trim_end()
.strip_suffix('}')
.expect("session JSON ends with }");
std::fs::write(&path, truncated).expect("truncate");
let decision =
decide_auto_resume(true, &ResumeRequest::default(), &fx.workspace, &fx.manager);
assert!(
decision.starts_fresh(),
"a corrupt session must not block startup"
);
assert!(matches!(decision, AutoResumeDecision::Unreadable { .. }));
}
#[test]
fn auto_resume_disabled_by_default() {
let fx = Fixture::new();
fx.save("ignored", "Not resumed", &fx.workspace);
let decision =
decide_auto_resume(false, &ResumeRequest::default(), &fx.workspace, &fx.manager);
assert_eq!(decision, AutoResumeDecision::Disabled);
assert_eq!(
decision.status_message(),
None,
"the default must be silent"
);
}
#[test]
fn auto_resume_never_crosses_a_workspace() {
let fx = Fixture::new();
let other = fx.dir.path().join("other");
std::fs::create_dir_all(&other).expect("other workspace");
fx.save("foreign", "Someone else's project", &other);
let decision =
decide_auto_resume(true, &ResumeRequest::default(), &fx.workspace, &fx.manager);
assert!(decision.starts_fresh());
assert_ne!(decision.session_id(), Some("foreign"));
}
#[test]
fn search_sort_and_preview_are_one_projection() {
let fx = Fixture::new();
fx.save("alpha", "Alpha lane work", &fx.workspace);
fx.save("beta", "Beta lane work", &fx.workspace);
let all = fx.manager.list_sessions().expect("list");
let deterministic = vec![
metadata_row("alpha", "Alpha lane work", "/repo"),
metadata_row("beta", "Beta lane work", "/repo"),
];
let searched = project_sessions(
&deterministic,
&SessionQuery::default()
.scoped_to(Path::new("/repo"))
.with_search("alpha lane"),
None,
);
assert_eq!(
searched.iter().map(|s| s.id.as_str()).collect::<Vec<_>>(),
vec!["alpha"],
"a substring of one title must not drag the other row in"
);
assert!(
project_sessions(
&deterministic,
&SessionQuery::default().with_search("no-such-session"),
None,
)
.is_empty()
);
let by_name = project_sessions(
&all,
&SessionQuery::default()
.scoped_to(&fx.workspace)
.with_sort(SessionSortMode::Name),
None,
);
assert_eq!(
by_name.iter().map(|s| s.title.as_str()).collect::<Vec<_>>(),
vec!["Alpha lane work", "Beta lane work"]
);
assert!(by_name.iter().all(|s| s.preview == s.title));
}
#[test]
fn rename_persists_and_survives_reload() {
let fx = Fixture::new();
fx.save("renameable", "Original title", &fx.workspace);
let renamed = fx
.manager
.rename_session("renameable", " Renamed title ", SessionMutator::Owner)
.expect("rename");
assert_eq!(renamed.title, "Renamed title", "titles are trimmed");
let reloaded = fx.reopen().load_session("renameable").expect("reload");
assert_eq!(reloaded.metadata.title, "Renamed title");
assert_eq!(
reloaded.metadata.created_at, renamed.created_at,
"rename must not disturb creation time"
);
assert!(
fx.manager
.rename_session("renameable", " ", SessionMutator::Owner)
.is_err(),
"an empty title must be rejected, not silently applied"
);
assert!(
fx.manager
.rename_session("renameable", &"x".repeat(101), SessionMutator::Owner)
.is_err(),
"an over-long title must be rejected"
);
}
#[test]
fn archive_is_durable_reversible_and_hidden_by_default() {
let fx = Fixture::new();
fx.save("keep", "Active work", &fx.workspace);
fx.save("putaway", "Finished work", &fx.workspace);
let archived = fx
.manager
.set_session_archived("putaway", true, SessionMutator::Owner)
.expect("archive");
assert!(archived.archived);
let reopened = fx.reopen();
assert!(
reopened
.load_session("putaway")
.expect("reload")
.metadata
.archived
);
let listed = reopened.list_sessions().expect("list");
let ids_for = |filter| {
crate::session_projection::select_sessions(
&listed,
&crate::session_projection::SessionQuery::default().with_filter(filter),
)
.into_iter()
.map(|s| s.id.clone())
.collect::<Vec<_>>()
};
assert_eq!(ids_for(SessionListFilter::ActiveOnly), vec!["keep"]);
assert_eq!(ids_for(SessionListFilter::ArchivedOnly), vec!["putaway"]);
assert_eq!(ids_for(SessionListFilter::IncludeArchived).len(), 2);
assert_ne!(
decide_auto_resume(true, &ResumeRequest::default(), &fx.workspace, &reopened)
.session_id(),
Some("putaway")
);
let restored = reopened
.set_session_archived("putaway", false, SessionMutator::Owner)
.expect("restore");
assert!(!restored.archived);
let relisted = reopened.list_sessions().expect("list");
assert_eq!(
crate::session_projection::select_sessions(
&relisted,
&crate::session_projection::SessionQuery::default()
.with_filter(SessionListFilter::ActiveOnly),
)
.len(),
2
);
}
#[test]
fn tui_and_api_listings_agree() {
let _lock = crate::test_support::lock_test_env();
let tmp = TempDir::new().expect("tempdir");
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path());
let manager = SessionManager::default_location().expect("manager");
let workspace = tmp.path().join("workspace");
let other = tmp.path().join("other");
std::fs::create_dir_all(&workspace).expect("workspace");
std::fs::create_dir_all(&other).expect("other");
for (id, title, ws) in [
("alpha", "Alpha work", &workspace),
("bravo", "Bravo work", &workspace),
("charlie", "Charlie work", &workspace),
("foreign", "Another project", &other),
] {
manager.save_session(&saved(id, title, ws)).expect("save");
}
let all = manager.list_sessions().expect("list");
let mut picker = crate::tui::session_picker::SessionPickerView::new(
&workspace,
crate::localization::Locale::En,
);
assert_eq!(
picker.visible_session_ids(),
api_ids(&all, &picker.view_query_for_test()),
"picker default view must equal the API projection of the same query"
);
assert!(
!picker
.visible_session_ids()
.contains(&"foreign".to_string()),
"workspace scope must exclude another project"
);
for _ in 0..3 {
picker.cycle_sort_for_test();
assert_eq!(
picker.visible_session_ids(),
api_ids(&all, &picker.view_query_for_test()),
"picker and API must agree after cycling sort"
);
}
picker.set_search_for_test("brav");
assert_eq!(picker.visible_session_ids(), vec!["bravo".to_string()]);
assert_eq!(
picker.visible_session_ids(),
api_ids(&all, &picker.view_query_for_test())
);
picker.set_search_for_test("");
picker.toggle_all_workspaces();
assert_eq!(
picker.visible_session_ids(),
api_ids(&all, &picker.view_query_for_test())
);
assert!(
picker
.visible_session_ids()
.contains(&"foreign".to_string()),
"broadening scope must reach the other workspace"
);
}
fn api_ids(
sessions: &[crate::session_manager::SessionMetadata],
query: &SessionQuery,
) -> Vec<String> {
project_sessions(sessions, query, None)
.into_iter()
.map(|row| row.id)
.collect()
}
#[test]
fn a_nested_path_resolves_to_the_same_scope_as_its_repository_root() {
let fx = Fixture::new();
let repo = fx.dir.path().join("repo");
let nested = repo.join("crates").join("tui");
std::fs::create_dir_all(&nested).expect("nested");
std::fs::create_dir_all(repo.join(".git")).expect("git dir");
std::fs::write(repo.join(".git").join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
fx.save("root", "At the repo root", &repo);
let all = fx.manager.list_sessions().expect("list");
let from_nested = project_sessions(&all, &SessionQuery::default().scoped_to(&nested), None);
assert_eq!(
from_nested
.iter()
.map(|r| r.id.as_str())
.collect::<Vec<_>>(),
vec!["root"],
"a session saved at the repo root must be in scope from a nested path"
);
}
#[test]
fn archive_survives_the_next_autosave() {
let fx = Fixture::new();
fx.save("live", "Being worked on", &fx.workspace);
let stale = fx
.manager
.list_sessions()
.expect("list")
.into_iter()
.find(|m| m.id == "live")
.expect("metadata");
assert!(!stale.archived);
fx.manager
.set_session_archived("live", true, SessionMutator::Owner)
.expect("archive");
let mut autosaved = stale.clone();
autosaved.message_count = 99;
assert!(fx.manager.merge_persisted_lifecycle(&mut autosaved));
assert!(
autosaved.archived,
"autosave must not revert an archive applied after its snapshot"
);
assert_eq!(
autosaved.message_count, 99,
"merging lifecycle state must not clobber conversation state"
);
}
#[test]
fn rename_survives_the_next_autosave() {
let fx = Fixture::new();
fx.save("live", "Before", &fx.workspace);
let stale = fx
.manager
.list_sessions()
.expect("list")
.into_iter()
.find(|m| m.id == "live")
.expect("metadata");
fx.manager
.rename_session("live", "After", SessionMutator::Owner)
.expect("rename");
let mut autosaved = stale.clone();
assert!(fx.manager.merge_persisted_lifecycle(&mut autosaved));
assert_eq!(autosaved.title, "After");
}
#[test]
fn an_external_writer_is_refused_while_a_session_is_live() {
let _lock = crate::test_support::lock_test_env();
let fx = Fixture::new();
fx.save("owned", "Open in the TUI", &fx.workspace);
crate::session_manager::set_live_session(Some("owned"));
let refused = fx
.manager
.set_session_archived("owned", true, SessionMutator::External)
.expect_err("external write must be refused while the session is live");
assert_eq!(refused.kind(), std::io::ErrorKind::ResourceBusy);
assert!(
!fx.manager
.load_session("owned")
.expect("reload")
.metadata
.archived,
"a refused write must not have partially applied"
);
let refused_rename = fx
.manager
.rename_session("owned", "Nope", SessionMutator::External)
.expect_err("external rename must be refused too");
assert_eq!(refused_rename.kind(), std::io::ErrorKind::ResourceBusy);
assert!(
fx.manager
.set_session_archived("owned", true, SessionMutator::Owner)
.is_ok()
);
crate::session_manager::set_live_session(None);
assert!(
fx.manager
.rename_session("owned", "Now allowed", SessionMutator::External)
.is_ok()
);
}
#[test]
fn auto_resume_skips_a_corrupt_newest_and_reports_how_many() {
let fx = Fixture::new();
fx.save("older", "Older but readable", &fx.workspace);
for id in ["broken-a", "broken-b"] {
let mut session = saved(id, "Damaged", &fx.workspace);
session.metadata.updated_at = chrono::Utc::now() + chrono::Duration::minutes(10);
fx.manager.save_session(&session).expect("save");
let path = fx.manager.sessions_dir().join(format!("{id}.json"));
let content = std::fs::read_to_string(&path).expect("read");
let truncated = content.trim_end().strip_suffix('}').expect("closing brace");
std::fs::write(&path, truncated).expect("truncate");
}
let decision =
decide_auto_resume(true, &ResumeRequest::default(), &fx.workspace, &fx.manager);
assert_eq!(
decision.session_id(),
Some("older"),
"one corrupt newest session must not cost the user every older one"
);
assert!(
matches!(
decision,
AutoResumeDecision::Resume {
skipped_unreadable: 2,
..
}
),
"the receipt must count what was skipped, got {decision:?}"
);
let receipt = decision.status_message().expect("receipt");
assert!(
receipt.contains("skipped 2 unreadable"),
"receipt must name the skipped count: {receipt}"
);
}
#[test]
fn auto_resume_reports_when_every_candidate_is_unreadable() {
let fx = Fixture::new();
for id in ["aa", "bb"] {
fx.save(id, "Damaged", &fx.workspace);
let path = fx.manager.sessions_dir().join(format!("{id}.json"));
let content = std::fs::read_to_string(&path).expect("read");
let truncated = content.trim_end().strip_suffix('}').expect("closing brace");
std::fs::write(&path, truncated).expect("truncate");
}
let decision =
decide_auto_resume(true, &ResumeRequest::default(), &fx.workspace, &fx.manager);
assert!(decision.starts_fresh());
assert!(matches!(
decision,
AutoResumeDecision::Unreadable {
skipped_unreadable: 2,
..
}
));
}
#[test]
fn auto_resume_candidate_walk_is_bounded() {
const {
assert!(
crate::session_resume::MAX_AUTO_RESUME_CANDIDATES <= 16,
"a damaged sessions directory must not turn startup into a long scan"
);
}
}
#[test]
fn session_and_thread_archive_filters_share_one_resolution() {
use crate::runtime_threads::ThreadListFilter;
for (include, only, expected_session, expected_thread) in [
(
None,
None,
SessionListFilter::ActiveOnly,
ThreadListFilter::ActiveOnly,
),
(
Some(true),
None,
SessionListFilter::IncludeArchived,
ThreadListFilter::IncludeArchived,
),
(
None,
Some(true),
SessionListFilter::ArchivedOnly,
ThreadListFilter::ArchivedOnly,
),
(
Some(true),
Some(true),
SessionListFilter::ArchivedOnly,
ThreadListFilter::ArchivedOnly,
),
] {
assert_eq!(
SessionListFilter::from_query(include, only),
expected_session
);
let thread = if only.unwrap_or(false) {
ThreadListFilter::ArchivedOnly
} else if include.unwrap_or(false) {
ThreadListFilter::IncludeArchived
} else {
ThreadListFilter::ActiveOnly
};
assert_eq!(thread, expected_thread);
}
}
#[test]
fn browsing_and_resume_are_offline() {
for (name, source) in [
("session_projection", include_str!("session_projection.rs")),
("session_resume", include_str!("session_resume.rs")),
] {
for forbidden in [
"reqwest",
"llm_client",
"ApiProvider",
"http://",
"https://",
] {
assert!(
!source.contains(forbidden),
"{name} must stay offline but references `{forbidden}`"
);
}
}
}
#[test]
fn history_and_search_results_are_bounded() {
let fx = Fixture::new();
for i in 0..30 {
fx.save(&format!("s{i:02}"), &format!("Session {i}"), &fx.workspace);
}
let all = fx.manager.list_sessions().expect("list");
let capped = project_sessions(
&all,
&SessionQuery::default()
.scoped_to(&fx.workspace)
.with_limit(5),
None,
);
assert_eq!(capped.len(), 5);
let unbounded = project_sessions(
&all,
&SessionQuery::default()
.scoped_to(&fx.workspace)
.with_limit(usize::MAX),
None,
);
assert!(unbounded.len() <= crate::session_projection::MAX_PROJECTED_SESSIONS);
let long = "x".repeat(5_000);
fx.save("long", &long, &fx.workspace);
let rows = project_sessions(
&fx.manager.list_sessions().expect("list"),
&SessionQuery::default().scoped_to(&fx.workspace),
None,
);
let long_row = rows.iter().find(|r| r.id == "long").expect("long row");
assert!(long_row.title.chars().count() <= 140);
assert!(long_row.preview.chars().count() <= 140);
}
#[test]
fn projections_never_fabricate_live_state() {
let fx = Fixture::new();
fx.save("recorded", "Recorded work", &fx.workspace);
let all = fx.manager.list_sessions().expect("list");
let anonymous = project_sessions(
&all,
&SessionQuery::default().scoped_to(&fx.workspace),
None,
);
assert!(
anonymous.iter().all(|row| !row.is_current),
"current-ness comes from the caller, never inferred from disk"
);
let row = &anonymous[0];
let metadata = all.iter().find(|m| m.id == row.id).expect("metadata");
assert_eq!(row.message_count, metadata.message_count);
assert_eq!(row.updated_at, metadata.updated_at);
assert_eq!(row.total_tokens, metadata.total_tokens);
assert_eq!(row.archived, metadata.archived);
}
}