use std::path::Path;
use crate::core::manifest::HarnessPlan;
use crate::core::paths::FrameworkPaths;
use crate::core::update_check::{
CatalogHashes, DeployedAgentHashes, StalenessReport, detect_for_framework,
};
use crate::session_manager::SessionRecord;
pub fn session_workdir(record: &SessionRecord) -> &Path {
record
.workspace_path
.as_deref()
.unwrap_or(record.cwd.as_path())
}
pub fn session_asset_staleness(record: &SessionRecord) -> StalenessReport {
let workdir = session_workdir(record);
let fw = FrameworkPaths::for_managed_workspace(workdir);
detect_for_framework(&fw, workdir)
}
pub fn session_plan(record: &SessionRecord) -> (FrameworkPaths, HarnessPlan) {
let workdir = session_workdir(record);
let fw = FrameworkPaths::for_managed_workspace(workdir);
let catalog_root = crate::content::catalog_root_for(&fw.root);
let sources = crate::core::manifest::ManifestSources::resolve(workdir, &catalog_root);
let manifest = crate::core::manifest::resolve_manifest(&sources);
let plan = HarnessPlan::from_manifest(&manifest, &fw, &catalog_root);
(fw, plan)
}
pub fn session_asset_staleness_with_catalog(
fw: &FrameworkPaths,
plan: &HarnessPlan,
catalog: &CatalogHashes,
) -> StalenessReport {
let agents_dir = fw.agent_deploy_dir();
let deployed_agents = DeployedAgentHashes::lazy(
crate::core::agent_manifest::AgentManifest::load(&agents_dir),
&agents_dir,
);
session_asset_staleness_with_shared(fw, plan, catalog, &deployed_agents)
}
pub fn session_asset_staleness_with_shared(
fw: &FrameworkPaths,
plan: &HarnessPlan,
catalog: &CatalogHashes,
deployed_agents: &DeployedAgentHashes,
) -> StalenessReport {
let skills_dir = fw.claude_skills_dir();
let deployed_skills = match crate::core::skill_manifest::SkillManifest::load(&skills_dir) {
Ok(m) => m,
Err(e) => {
tracing::warn!(
dir = %skills_dir.display(),
error = %e,
"skill ownership ledger unreadable — staleness is undetermined"
);
return StalenessReport {
unknown: true,
..StalenessReport::default()
};
}
};
catalog.detect(
deployed_agents,
&deployed_skills,
&skills_dir,
|name| plan.agent_selected(name),
|name| plan.skill_selected(name),
|name| plan.agent_staleness_ignored(name),
)
}
pub fn session_assets_stale(record: &SessionRecord) -> bool {
session_asset_staleness(record).stale
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::agent_deployer::deploy_agents_filtered;
use crate::session_manager::{ManagedSessionId, ManagedSessionState};
use chrono::Utc;
use std::path::PathBuf;
use tempfile::TempDir;
fn make_record(workspace: Option<PathBuf>, cwd: PathBuf) -> SessionRecord {
SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tm-test".to_string(),
cwd,
task: "test".to_string(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: workspace,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
}
}
#[test]
fn session_workdir_prefers_workspace_path() {
let ws = PathBuf::from("/workspace");
let record = make_record(Some(ws.clone()), PathBuf::from("/cwd"));
assert_eq!(session_workdir(&record), ws.as_path());
}
#[test]
fn session_workdir_falls_back_to_cwd() {
let cwd = PathBuf::from("/cwd");
let record = make_record(None, cwd.clone());
assert_eq!(session_workdir(&record), cwd.as_path());
}
struct HomeGuard(Option<String>);
impl Drop for HomeGuard {
fn drop(&mut self) {
match self.0 {
Some(ref p) => unsafe { std::env::set_var("HOME", p) },
None => unsafe { std::env::remove_var("HOME") },
}
}
}
fn fake_home() -> (TempDir, HomeGuard) {
let home = TempDir::new().unwrap();
let prior = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", home.path()) };
(home, HomeGuard(prior))
}
#[test]
#[serial_test::serial]
fn session_asset_staleness_ok_when_fresh() {
let (home, _guard) = fake_home();
let fw = FrameworkPaths::default();
let bundled = fw.agent_source_dir();
std::fs::create_dir_all(&bundled).unwrap();
std::fs::write(bundled.join("rust-engineer.md"), "v1 body").unwrap();
let workspace = home.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let session_fw = FrameworkPaths::for_managed_workspace(&workspace);
deploy_agents_filtered(&bundled, &session_fw.agent_deploy_dir(), |_| true).unwrap();
let record = make_record(Some(workspace.clone()), workspace);
assert!(
!session_assets_stale(&record),
"freshly deployed session must not be stale"
);
}
#[test]
#[serial_test::serial]
fn session_asset_staleness_flags_catalog_drift() {
let (home, _guard) = fake_home();
let fw = FrameworkPaths::default();
let bundled = fw.agent_source_dir();
std::fs::create_dir_all(&bundled).unwrap();
std::fs::write(bundled.join("rust-engineer.md"), "v1 body").unwrap();
let workspace = home.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let session_fw = FrameworkPaths::for_managed_workspace(&workspace);
deploy_agents_filtered(&bundled, &session_fw.agent_deploy_dir(), |_| true).unwrap();
let record = make_record(Some(workspace.clone()), workspace);
assert!(!session_assets_stale(&record));
std::fs::write(bundled.join("rust-engineer.md"), "v2 body — catalog moved").unwrap();
assert!(
session_assets_stale(&record),
"session must be reported stale once the bundled source changed"
);
}
#[test]
#[serial_test::serial]
fn session_plan_resolves_default_bundled_source() {
let (home, _guard) = fake_home();
let workspace = home.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let record = make_record(Some(workspace.clone()), workspace);
let (fw, plan) = session_plan(&record);
assert_eq!(plan.agent_source, fw.agent_source_dir());
assert_eq!(plan.skill_source, fw.skill_source_dir());
}
#[test]
#[serial_test::serial]
fn session_asset_staleness_with_catalog_matches_uncached_result() {
let (home, _guard) = fake_home();
let fw = FrameworkPaths::default();
let bundled = fw.agent_source_dir();
std::fs::create_dir_all(&bundled).unwrap();
std::fs::write(bundled.join("rust-engineer.md"), "v1 body").unwrap();
let workspace = home.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let session_fw = FrameworkPaths::for_managed_workspace(&workspace);
deploy_agents_filtered(&bundled, &session_fw.agent_deploy_dir(), |_| true).unwrap();
std::fs::write(bundled.join("rust-engineer.md"), "v2 body").unwrap();
let record = make_record(Some(workspace.clone()), workspace);
let uncached = session_asset_staleness(&record);
let (session_fw, plan) = session_plan(&record);
let catalog = crate::core::update_check::CatalogHashes::compute(
&plan.agent_source,
&plan.skill_source,
);
let cached = session_asset_staleness_with_catalog(&session_fw, &plan, &catalog);
assert_eq!(uncached.stale, cached.stale);
assert!(
cached.stale,
"the catalog drift must be detected either way"
);
}
#[test]
#[serial_test::serial]
fn session_asset_staleness_with_shared_matches_unshared() {
let (home, _guard) = fake_home();
let fw = FrameworkPaths::default();
let bundled = fw.agent_source_dir();
std::fs::create_dir_all(&bundled).unwrap();
std::fs::write(bundled.join("rust-engineer.md"), "v1 body").unwrap();
let workspace = home.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let session_fw = FrameworkPaths::for_managed_workspace(&workspace);
deploy_agents_filtered(&bundled, &session_fw.agent_deploy_dir(), |_| true).unwrap();
std::fs::write(bundled.join("rust-engineer.md"), "v2 body — catalog moved").unwrap();
let record = make_record(Some(workspace.clone()), workspace);
let (fw_s, plan) = session_plan(&record);
let catalog = CatalogHashes::compute(&plan.agent_source, &plan.skill_source);
let unshared = session_asset_staleness_with_catalog(&fw_s, &plan, &catalog);
let shared_agents = DeployedAgentHashes::read(&fw_s.agent_deploy_dir(), &catalog);
let shared = session_asset_staleness_with_shared(&fw_s, &plan, &catalog, &shared_agents);
assert!(unshared.stale, "fixture must be genuinely stale");
assert_eq!(
unshared.stale, shared.stale,
"sharing the deployed-agent read must not change the verdict"
);
assert_eq!(
unshared.changes, shared.changes,
"sharing the deployed-agent read must not change WHICH artifacts \
are reported as drifted"
);
}
}