use std::{
io::{Read, Write},
net::{TcpListener, TcpStream},
path::{Path, PathBuf},
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
thread,
};
use basis::{
AllowAll, CollectingSink, Config, ContextConfig, Effort, MemoryConfig, Runtime, Workspace,
WorkspaceBuilder, hooks::HooksConfig, skills::SkillsConfig, templates::TemplatesConfig,
tools::declared::ToolsConfig,
};
use mentra::ModelSelector;
fn pinned(workspace: &Path) -> WorkspaceBuilder {
Workspace::builder(workspace)
.with_context(ContextConfig {
file_name: "AGENTS.md".to_string(),
global_dir: None,
walk_parents: false,
})
.with_skills(SkillsConfig {
workspace_subdir: Some(PathBuf::from(".basis/skills")),
shared_workspace_dir: true,
global_dir: None,
shared_home_dir: false,
})
.with_templates(TemplatesConfig {
workspace_subdir: PathBuf::from(".basis/templates"),
global_dir: None,
})
.with_hooks(HooksConfig {
workspace_file: PathBuf::from(".basis/hooks.json"),
global_dir: None,
supplied: Vec::new(),
})
.with_tools(ToolsConfig {
workspace_file: PathBuf::from(".basis/tools.json"),
global_dir: None,
supplied: Vec::new(),
})
.with_memory(MemoryConfig::disabled())
}
fn offline() -> Arc<Runtime> {
Arc::new(
Runtime::builder()
.with_base_url("http://127.0.0.1:1/v1")
.with_api_key("test-key")
.with_ephemeral_history()
.build()
.expect("builds offline"),
)
}
fn write_config(workspace: &Path, body: &str) {
let path = workspace.join(".basis").join("config.json");
std::fs::create_dir_all(path.parent().expect("a parent")).expect("create .basis");
std::fs::write(path, body).expect("write config");
}
#[tokio::test]
async fn a_workspace_file_decides_the_model_when_nothing_else_did() {
let dir = tempfile::tempdir().expect("tempdir");
write_config(dir.path(), r#"{"schema": 1, "model": "from-the-file"}"#);
let workspace = pinned(dir.path())
.with_runtime(offline())
.open()
.await
.expect("opens offline");
assert_eq!(workspace.model(), "from-the-file");
assert_eq!(workspace.config_files().len(), 1);
assert_eq!(workspace.config_files()[0].scope, "workspace");
assert_eq!(
workspace
.config()
.model
.as_ref()
.map(|model| model.value.as_str()),
Some("from-the-file"),
"the workspace keeps the answer and the file that gave it"
);
}
#[tokio::test]
async fn an_explicit_model_outranks_the_file() {
let dir = tempfile::tempdir().expect("tempdir");
write_config(dir.path(), r#"{"schema": 1, "model": "from-the-file"}"#);
let workspace = pinned(dir.path())
.with_runtime(offline())
.with_model(ModelSelector::Id("from-the-caller".to_string()))
.open()
.await
.expect("opens offline");
assert_eq!(workspace.model(), "from-the-caller");
}
#[tokio::test]
async fn an_empty_config_is_the_off_switch() {
let dir = tempfile::tempdir().expect("tempdir");
write_config(dir.path(), r#"{"schema": 1, "model": "from-the-file"}"#);
let runtime = Arc::new(
Runtime::builder()
.with_base_url("http://127.0.0.1:1/v1")
.with_api_key("test-key")
.with_ephemeral_history()
.with_model(ModelSelector::Id("the-runtime-policy".to_string()))
.build()
.expect("builds offline"),
);
let workspace = pinned(dir.path())
.with_runtime(runtime)
.with_config(Config::default())
.open()
.await
.expect("opens offline");
assert_eq!(workspace.model(), "the-runtime-policy");
assert!(
workspace.config_files().is_empty(),
"nothing was read, so nothing may be reported"
);
}
#[tokio::test]
async fn a_base_url_in_a_committed_file_fails_the_open_by_name() {
let dir = tempfile::tempdir().expect("tempdir");
write_config(
dir.path(),
r#"{"schema": 1, "base_url": "http://127.0.0.1:1/v1"}"#,
);
let error = pinned(dir.path())
.with_runtime(offline())
.open()
.await
.expect_err("refused");
let rendered = error.to_string();
assert!(rendered.contains("config.json"), "{rendered}");
assert!(rendered.contains("base_url"), "{rendered}");
}
#[tokio::test]
async fn a_malformed_file_fails_the_open_rather_than_running_another_model() {
let dir = tempfile::tempdir().expect("tempdir");
write_config(dir.path(), "{not json");
let error = pinned(dir.path())
.with_runtime(offline())
.open()
.await
.expect_err("refused");
assert!(error.to_string().contains("config.json"), "{error}");
}
#[tokio::test]
async fn the_workspace_files_effort_reaches_the_provider() {
let endpoint = ScriptedEndpoint::start();
let dir = tempfile::tempdir().expect("tempdir");
write_config(
dir.path(),
r#"{"schema": 1, "model": "test-model", "effort": "high"}"#,
);
let workspace = pinned(dir.path())
.with_runtime(endpoint.runtime())
.open()
.await
.expect("opens against the scripted endpoint");
let mut run = workspace.prepare("go").expect("mints");
run.execute_with_approver(CollectingSink::default(), AllowAll)
.await
.expect("the scripted turn runs");
let request = endpoint.first_request();
assert!(
request.contains(r#""reasoning_effort":"high""#),
"the file's effort never reached the request: {request}"
);
}
#[tokio::test]
async fn a_run_that_asked_for_an_effort_keeps_its_own() {
let endpoint = ScriptedEndpoint::start();
let dir = tempfile::tempdir().expect("tempdir");
write_config(
dir.path(),
r#"{"schema": 1, "model": "test-model", "effort": "high"}"#,
);
let workspace = pinned(dir.path())
.with_runtime(endpoint.runtime())
.open()
.await
.expect("opens against the scripted endpoint");
let mut run = workspace
.prepare(basis::workspace::RunSpec::new("go").with_effort(Effort::Low))
.expect("mints");
run.execute_with_approver(CollectingSink::default(), AllowAll)
.await
.expect("the scripted turn runs");
let request = endpoint.first_request();
assert!(
request.contains(r#""reasoning_effort":"low""#),
"the run's own answer must win: {request}"
);
}
#[tokio::test]
async fn a_run_reports_the_effort_it_was_opened_at() {
let dir = tempfile::tempdir().expect("tempdir");
write_config(
dir.path(),
r#"{"schema": 1, "model": "test-model", "effort": "high"}"#,
);
let workspace = pinned(dir.path())
.with_runtime(offline())
.open()
.await
.expect("opens");
let mut run = workspace.prepare("go").expect("mints");
assert_eq!(run.effort(), Some(Effort::High));
run.set_effort(Some(Effort::Low)).expect("sets");
assert_eq!(run.effort(), Some(Effort::Low), "and it follows a change");
run.set_effort(None).expect("clears");
assert_eq!(
run.effort(),
None,
"cleared means the provider's own default, which basis has no name for"
);
}
#[tokio::test]
async fn a_run_nobody_asked_an_effort_of_reports_none() {
let dir = tempfile::tempdir().expect("tempdir");
write_config(dir.path(), r#"{"schema": 1, "model": "test-model"}"#);
let workspace = pinned(dir.path())
.with_runtime(offline())
.open()
.await
.expect("opens");
assert_eq!(workspace.prepare("go").expect("mints").effort(), None);
}
struct ScriptedEndpoint {
base_url: String,
requests: Arc<Mutex<Vec<String>>>,
}
impl ScriptedEndpoint {
fn start() -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test endpoint");
let address = listener.local_addr().expect("read endpoint address");
let requests = Arc::new(Mutex::new(Vec::new()));
let recorded = Arc::clone(&requests);
let turns = Arc::new(AtomicUsize::new(0));
thread::spawn(move || {
while let Ok((stream, _)) = listener.accept() {
let turns = Arc::clone(&turns);
let recorded = Arc::clone(&recorded);
thread::spawn(move || answer(stream, &turns, &recorded));
}
});
Self {
base_url: format!("http://{address}/"),
requests,
}
}
fn runtime(&self) -> Arc<Runtime> {
Arc::new(
Runtime::builder()
.with_base_url(&self.base_url)
.with_api_key("test-key")
.with_ephemeral_history()
.build()
.expect("builds against the scripted endpoint"),
)
}
fn first_request(&self) -> String {
self.requests
.lock()
.expect("requests")
.first()
.cloned()
.expect("the model was asked something")
}
}
fn model_listing(request: &str) -> Option<String> {
let line = request.lines().next()?;
let target = line.split_whitespace().nth(1)?;
(line.starts_with("GET ") && target.ends_with("/models")).then(|| {
let body = r#"{"object":"list","data":[{"id":"test-model","object":"model"}]}"#;
format!(
"HTTP/1.1 200 OK\r\nconnection: close\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{body}",
body.len()
)
})
}
fn answer(mut stream: TcpStream, turns: &AtomicUsize, recorded: &Mutex<Vec<String>>) {
let request = read_http_request(&mut stream);
if let Some(listing) = model_listing(&request) {
let _ = stream.write_all(listing.as_bytes());
return;
}
let index = turns.fetch_add(1, Ordering::SeqCst) + 1;
recorded.lock().expect("requests").push(request);
let body = format!(
concat!(
"data: {{\"id\":\"chatcmpl_{0}\",\"model\":\"test-model\",\"choices\":[{{\"index\":0,\"delta\":{{\"role\":\"assistant\",\"content\":\"done\"}}}}]}}\n\n",
"data: {{\"id\":\"chatcmpl_{0}\",\"choices\":[{{\"index\":0,\"delta\":{{}},\"finish_reason\":\"stop\"}}]}}\n\n",
"data: [DONE]\n\n"
),
index
);
let response = format!(
"HTTP/1.1 200 OK\r\nconnection: close\r\ncontent-type: text/event-stream\r\ncontent-length: {}\r\n\r\n{body}",
body.len()
);
let _ = stream.write_all(response.as_bytes());
}
fn read_http_request(stream: &mut TcpStream) -> String {
let mut bytes = Vec::new();
let mut buffer = [0_u8; 4096];
let mut header_end = None;
let mut content_length = 0_usize;
loop {
let read = stream.read(&mut buffer).expect("read request");
if read == 0 {
break;
}
bytes.extend_from_slice(&buffer[..read]);
if header_end.is_none()
&& let Some(index) = bytes.windows(4).position(|window| window == b"\r\n\r\n")
{
let end = index + 4;
header_end = Some(end);
let headers = String::from_utf8_lossy(&bytes[..end]);
content_length = headers
.lines()
.find_map(|line| {
let (name, value) = line.split_once(':')?;
name.eq_ignore_ascii_case("content-length")
.then(|| value.trim().parse::<usize>().expect("content length"))
})
.unwrap_or_default();
}
if header_end.is_some_and(|end| bytes.len() >= end + content_length) {
break;
}
}
String::from_utf8(bytes).expect("request should be utf8")
}