use crate::config::{Config, NamedContextConfig, ProxyScopeConfig};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use super::session::TuneSessionStore;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplyReport {
pub config_path: PathBuf,
pub context_id: String,
pub source_run_id: String,
pub context_created: bool,
pub toml_applied: String,
}
pub fn apply_tune_export(
store: &TuneSessionStore,
session_id: &str,
agent_id: Option<&str>,
context_id: Option<&str>,
config_path: Option<&Path>,
) -> Result<ApplyReport, String> {
let artifact = store.export(session_id, agent_id, context_id)?;
let mut config = match config_path {
Some(p) => Config::load_from(
p.to_str()
.ok_or_else(|| format!("config path is not valid UTF-8: {}", p.display()))?,
)
.map_err(|e| format!("Failed to load config from {}: {}", p.display(), e))?,
None => Config::load().map_err(|e| format!("Failed to load default config: {}", e))?,
};
let new_scope: ProxyScopeConfig = serde_json::from_value(artifact.scope.clone())
.map_err(|e| format!("Failed to parse exported scope JSON: {}", e))?;
let ctx_existed = config.config.contexts.contains_key(&artifact.context_id);
let entry = config
.config
.contexts
.entry(artifact.context_id.clone())
.or_insert_with(NamedContextConfig::default);
entry.scope = new_scope;
let path_to_write = config_path
.map(Path::to_path_buf)
.unwrap_or_else(Config::local_config_path);
if let Some(p) = config_path {
let content = toml::to_string_pretty(&config.config)
.map_err(|e| format!("TOML serialize failed: {}", e))?;
std::fs::write(p, content)
.map_err(|e| format!("Failed to write {}: {}", p.display(), e))?;
} else {
config
.save()
.map_err(|e| format!("Failed to save local config: {}", e))?;
}
Ok(ApplyReport {
config_path: path_to_write,
context_id: artifact.context_id,
source_run_id: artifact.source_run_id.unwrap_or_default(),
context_created: !ctx_existed,
toml_applied: artifact.formats.toml,
})
}
pub fn apply_export_artifact(
artifact: &super::session::TuneExportArtifact,
config_path: Option<&Path>,
) -> Result<ApplyReport, String> {
let mut config = match config_path {
Some(p) => Config::load_from(
p.to_str()
.ok_or_else(|| format!("config path is not valid UTF-8: {}", p.display()))?,
)
.map_err(|e| format!("Failed to load config from {}: {}", p.display(), e))?,
None => Config::load().map_err(|e| format!("Failed to load default config: {}", e))?,
};
let new_scope: ProxyScopeConfig = serde_json::from_value(artifact.scope.clone())
.map_err(|e| format!("Failed to parse exported scope JSON: {}", e))?;
let ctx_existed = config.config.contexts.contains_key(&artifact.context_id);
let entry = config
.config
.contexts
.entry(artifact.context_id.clone())
.or_insert_with(NamedContextConfig::default);
entry.scope = new_scope;
let path_to_write = config_path
.map(Path::to_path_buf)
.unwrap_or_else(Config::local_config_path);
if let Some(p) = config_path {
let content = toml::to_string_pretty(&config.config)
.map_err(|e| format!("TOML serialize failed: {}", e))?;
std::fs::write(p, content)
.map_err(|e| format!("Failed to write {}: {}", p.display(), e))?;
} else {
config
.save()
.map_err(|e| format!("Failed to save local config: {}", e))?;
}
Ok(ApplyReport {
config_path: path_to_write,
context_id: artifact.context_id.clone(),
source_run_id: artifact.source_run_id.clone().unwrap_or_default(),
context_created: !ctx_existed,
toml_applied: artifact.formats.toml.clone(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::WeightedPhrase;
use crate::proxy::tune::scope::ScopeTuneParams;
use crate::proxy::tune::scope_tune;
use crate::proxy::tune::TuneBudget;
use crate::proxy::types::SearchResult;
use tempfile::TempDir;
fn make_hit(id: &str, score: f32, content: &str) -> SearchResult {
SearchResult {
id: id.to_string(),
score,
content: content.to_string(),
metadata: None,
upstream_id: None,
}
}
fn open_session_with_scope_run(
store: &TuneSessionStore,
agent_id: &str,
context_id: &str,
) -> String {
let sess = store
.open(agent_id.to_string(), context_id.to_string(), None)
.unwrap();
let hits = vec![make_hit("d1", 1.0, "rust async runtime")];
let weighted_phrases = vec![WeightedPhrase {
text: "rust".to_string(),
weight: 2.0,
min_similarity: None,
}];
let _ = scope_tune(
store,
ScopeTuneParams {
session_id: sess.session_id.clone(),
agent_id: Some(agent_id.to_string()),
context_id: Some(context_id.to_string()),
hits,
weighted_phrases,
mode: Some("filter".to_string()),
min_similarity: Some(0.25),
min_similarity_sweep: None,
scope_weight: Some(0.3),
lexical_weight: None,
budget: TuneBudget::default(),
},
)
.unwrap();
sess.session_id
}
#[test]
fn test_apply_creates_new_context() {
let dir = TempDir::new().unwrap();
let cfg_path = dir.path().join("conproxy.toml");
let initial = r#"[proxy]
listen = "127.0.0.1:9999"
[upstreams.dummy]
url = "http://127.0.0.1:65535"
type = "meilisearch"
"#;
std::fs::write(&cfg_path, initial).unwrap();
let store = TuneSessionStore::new(3600);
let sid = open_session_with_scope_run(&store, "alice", "default");
let report = apply_tune_export(
&store,
&sid,
Some("alice"),
Some("default"),
Some(&cfg_path),
)
.unwrap();
assert!(report.context_created);
assert_eq!(report.context_id, "default");
assert!(report.toml_applied.contains("[contexts.default.scope]"));
assert!(report.toml_applied.contains("mode = \"filter\""));
assert!(report.toml_applied.contains("text = \"rust\""));
let on_disk = std::fs::read_to_string(&cfg_path).unwrap();
assert!(on_disk.contains("[contexts.default.scope]"));
assert!(on_disk.contains("text = \"rust\""));
assert!(on_disk.contains("listen = \"127.0.0.1:9999\"")); }
#[test]
fn test_apply_overwrites_existing_scope() {
let dir = TempDir::new().unwrap();
let cfg_path = dir.path().join("conproxy.toml");
let initial = r#"[proxy]
listen = "127.0.0.1:9999"
[upstreams.dummy]
url = "http://127.0.0.1:65535"
type = "meilisearch"
[contexts.default]
default = true
[[contexts.default.upstreams]]
ref = "dummy"
[contexts.default.scope]
mode = "boost"
min_similarity = 0.1
"#;
std::fs::write(&cfg_path, initial).unwrap();
let store = TuneSessionStore::new(3600);
let sid = open_session_with_scope_run(&store, "alice", "default");
let report = apply_tune_export(
&store,
&sid,
Some("alice"),
Some("default"),
Some(&cfg_path),
)
.unwrap_or_else(|e| panic!("apply failed: {e}"));
assert!(!report.context_created, "context already existed");
let on_disk = std::fs::read_to_string(&cfg_path).unwrap();
assert!(on_disk.contains("mode = \"filter\""));
assert!(!on_disk.contains("min_similarity = 0.1"));
assert!(on_disk.contains("default = true"));
}
#[test]
fn test_apply_unknown_session_errors() {
let dir = TempDir::new().unwrap();
let cfg_path = dir.path().join("conproxy.toml");
std::fs::write(&cfg_path, "[proxy]\nlisten = \"127.0.0.1:9999\"\n").unwrap();
let store = TuneSessionStore::new(3600);
let err = apply_tune_export(
&store,
"nope",
Some("alice"),
Some("default"),
Some(&cfg_path),
)
.unwrap_err();
assert!(err.contains("session not found") || err.contains("no runs"));
}
#[test]
fn test_apply_export_artifact_direct() {
let artifact = crate::proxy::tune::session::TuneExportArtifact {
session_id: "s1".to_string(),
agent_id: "bob".to_string(),
context_id: "ctx".to_string(),
scope: serde_json::json!({
"mode": "rerank",
"min_similarity": 0.5,
"weighted_phrases": [{"text": "embed", "weight": 1.5}],
}),
other: serde_json::json!({}),
formats: crate::proxy::tune::session::TuneExportFormats {
toml: "[contexts.ctx.scope]\nmode = \"rerank\"\n".to_string(),
json: serde_json::json!({}),
},
source_run_id: Some("run-x".to_string()),
};
let dir = TempDir::new().unwrap();
let cfg_path = dir.path().join("conproxy.toml");
std::fs::write(
&cfg_path,
"[proxy]\nlisten = \"127.0.0.1:9999\"\n\n[upstreams.dummy]\nurl = \"http://127.0.0.1:65535\"\ntype = \"fts\"\n",
)
.unwrap();
let report = apply_export_artifact(&artifact, Some(&cfg_path)).unwrap();
assert!(report.context_created);
assert_eq!(report.context_id, "ctx");
assert_eq!(report.source_run_id, "run-x");
let on_disk = std::fs::read_to_string(&cfg_path).unwrap();
assert!(on_disk.contains("[contexts.ctx.scope]"));
assert!(on_disk.contains("mode = \"rerank\""));
}
#[test]
fn test_apply_default_path_round_trip() {
let dir = TempDir::new().unwrap();
let _ = dir; let store = TuneSessionStore::new(3600);
let err = apply_tune_export(&store, "missing", Some("alice"), Some("default"), None);
assert!(err.is_err());
}
}