use anyhow::Result;
use std::collections::BTreeMap;
use crate::config;
use crate::gateway::{Clock, Filesystem, GitClient, LlmClient};
use crate::lockfile;
use crate::paths::ConfigPaths;
use crate::types::{InstallScope, LockFile, SourcesFile};
pub struct AppContext<'a> {
pub fs: &'a dyn Filesystem,
pub git: &'a dyn GitClient,
pub clock: &'a dyn Clock,
pub paths: &'a ConfigPaths,
pub llm: Option<&'a dyn LlmClient>,
}
impl<'a> AppContext<'a> {
pub fn with_paths<'b>(&self, paths: &'b ConfigPaths) -> AppContext<'b>
where
'a: 'b,
{
AppContext {
fs: self.fs,
git: self.git,
clock: self.clock,
paths,
llm: self.llm,
}
}
#[cfg(feature = "llm")]
pub fn with_llm<'b>(&self, llm: &'b dyn LlmClient) -> AppContext<'b>
where
'a: 'b,
{
AppContext {
fs: self.fs,
git: self.git,
clock: self.clock,
paths: self.paths,
llm: Some(llm),
}
}
}
pub struct LoadedState {
pub sources: SourcesFile,
pub locks: BTreeMap<InstallScope, LockFile>,
}
impl LoadedState {
pub fn load(ctx: &AppContext<'_>) -> Result<Self> {
let sources = config::load_sources(ctx.fs, ctx.paths)?;
let locks = lockfile::load_both(ctx.fs, ctx.paths)?;
Ok(Self { sources, locks })
}
pub fn lock(&self, scope: InstallScope) -> &LockFile {
&self.locks[&scope]
}
pub fn scopes(&self) -> impl Iterator<Item = (InstallScope, &LockFile)> {
self.locks.iter().map(|(s, l)| (*s, l))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gateway::fakes::{FakeClock, FakeFilesystem, FakeGitClient};
use crate::lockfile;
use crate::test_support::{make_lock_entry_builder, test_paths};
use crate::types::{ArtifactKind, InstallScope, LockFile};
use chrono::Utc;
use std::collections::BTreeMap;
fn make_ctx<'a>(
fs: &'a FakeFilesystem,
git: &'a FakeGitClient,
clock: &'a FakeClock,
paths: &'a ConfigPaths,
) -> AppContext<'a> {
AppContext {
fs,
git,
clock,
paths,
llm: None,
}
}
#[test]
fn loaded_state_load_empty_fs_returns_defaults() {
let paths = test_paths();
let fs = FakeFilesystem::new();
let git = FakeGitClient::new();
let clock = FakeClock::at(Utc::now());
let ctx = make_ctx(&fs, &git, &clock, &paths);
let state = LoadedState::load(&ctx).unwrap();
assert!(state.sources.sources.is_empty());
assert!(state.lock(InstallScope::Global).packages.is_empty());
assert!(state.lock(InstallScope::Local).packages.is_empty());
}
#[test]
fn loaded_state_lock_returns_scope_lockfile() {
let paths = test_paths();
let fs = FakeFilesystem::new();
let git = FakeGitClient::new();
let clock = FakeClock::at(Utc::now());
let entry = make_lock_entry_builder(ArtifactKind::Agent, "myrepo", "agents/my-agent.md");
let mut packages = BTreeMap::new();
packages.insert("my-agent".to_string(), entry);
let lock = LockFile {
version: 1,
packages,
};
lockfile::save(&lock, InstallScope::Global, &fs, &paths).unwrap();
let ctx = make_ctx(&fs, &git, &clock, &paths);
let state = LoadedState::load(&ctx).unwrap();
assert!(state.lock(InstallScope::Global).packages.contains_key("my-agent"));
assert!(state.lock(InstallScope::Local).packages.is_empty());
}
#[test]
fn loaded_state_scopes_global_first() {
let paths = test_paths();
let fs = FakeFilesystem::new();
let git = FakeGitClient::new();
let clock = FakeClock::at(Utc::now());
let ctx = make_ctx(&fs, &git, &clock, &paths);
let state = LoadedState::load(&ctx).unwrap();
let scopes: Vec<InstallScope> = state.scopes().map(|(s, _)| s).collect();
assert_eq!(scopes, vec![InstallScope::Global, InstallScope::Local]);
}
}