mod common;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use agentos_client::config::{AgentOsConfig, PackageRef};
use agentos_client::{
AgentOs, OpenSessionInput, PromptInput, ReadHistoryInput, SessionStreamEntry,
};
use agentos_vm_config::VmSqliteDescriptor;
use futures::StreamExt;
const MOCK_AGENT_TYPE: &str = "mock-agent";
const MOCK_SESSION_ID: &str = "mock-session-1";
const MOCK_PROMPT_TEXT: &str = "mock-session-pong";
const MOCK_ACP_ADAPTER: &str = r#"
let buffer = "";
function writeMessage(message) { process.stdout.write(JSON.stringify(message) + "\n"); }
function writeResponse(id, result) { writeMessage({ jsonrpc: "2.0", id, result }); }
process.stdin.resume();
process.stdin.on("data", (chunk) => {
buffer += chunk instanceof Uint8Array ? new TextDecoder().decode(chunk) : String(chunk);
while (true) {
const idx = buffer.indexOf("\n");
if (idx === -1) break;
const line = buffer.slice(0, idx);
buffer = buffer.slice(idx + 1);
if (!line.trim()) continue;
const msg = JSON.parse(line);
if (msg.id === undefined) continue;
switch (msg.method) {
case "initialize":
writeResponse(msg.id, {
protocolVersion: 1,
agentInfo: { name: "mock-agent", version: "1.0.0" },
agentCapabilities: { plan_mode: false, tool_calls: false, promptCapabilities: {} },
modes: { currentModeId: "default", availableModes: [{ id: "default", label: "Default" }] },
configOptions: [],
});
break;
case "session/new":
writeResponse(msg.id, {
sessionId: "__MOCK_SESSION_ID__",
modes: { currentModeId: "default", availableModes: [{ id: "default", label: "Default" }] },
configOptions: [],
});
break;
case "session/prompt":
writeMessage({ jsonrpc: "2.0", method: "session/update", params: {
sessionId: "__MOCK_SESSION_ID__",
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "__MOCK_PROMPT_TEXT__" } } } });
writeResponse(msg.id, { stopReason: "end_turn" });
break;
case "session/cancel":
writeResponse(msg.id, {});
break;
default:
writeMessage({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "Method not found" } });
break;
}
}
});
"#;
fn unique_dir(tag: &str) -> PathBuf {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time")
.as_nanos();
let dir = std::env::temp_dir().join(format!("agentos-session-e2e-{tag}-{nonce}"));
std::fs::create_dir_all(&dir).expect("create temp dir");
dir
}
fn write_mock_agent_package(root: &Path) -> PathBuf {
let package = root.join("mock-agent-package");
std::fs::create_dir_all(package.join("bin")).expect("create package bin");
std::fs::write(
package.join("agentos-package.json"),
r#"{"name":"mock-agent","version":"1.0.0","agent":{"acpEntrypoint":"mock-agent-acp"}}"#,
)
.expect("write agentos-package.json");
let adapter = MOCK_ACP_ADAPTER
.replace("__MOCK_SESSION_ID__", MOCK_SESSION_ID)
.replace("__MOCK_PROMPT_TEXT__", MOCK_PROMPT_TEXT);
let bin = package.join("bin/mock-agent-acp");
std::fs::write(&bin, format!("#!/usr/bin/env node\n{adapter}\n")).expect("write adapter");
let mut perms = std::fs::metadata(&bin).expect("stat adapter").permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&bin, perms).expect("chmod adapter");
package
}
#[tokio::test]
async fn durable_session_surface_persists_native_acp_history() {
if !common::require_sidecar("durable_session_surface_persists_native_acp_history") {
return;
}
let package_root = unique_dir("durable");
let package_dir = write_mock_agent_package(&package_root);
common::ensure_sidecar_env();
let os = AgentOs::create(AgentOsConfig {
database: Some(VmSqliteDescriptor::SqliteFile {
path: package_root
.join("agentos.sqlite")
.to_string_lossy()
.into_owned(),
}),
packages: vec![PackageRef {
path: package_dir.to_string_lossy().into_owned(),
}],
..Default::default()
})
.await
.expect("create durable VM");
os.open_session(OpenSessionInput {
session_id: None,
agent: MOCK_AGENT_TYPE.to_owned(),
cwd: Some(String::from("/home/agentos")),
additional_directories: None,
env: None,
mcp_servers: None,
permission_policy: None,
skip_os_instructions: None,
additional_instructions: None,
})
.await
.expect("open durable session");
let session = os.get_session(None).await.expect("get durable session");
assert_eq!(session.session_id, "main");
assert_eq!(
os.list_sessions(Default::default())
.await
.expect("list")
.sessions
.len(),
1
);
let (mut events, _subscription) = os.on_session_event(Some("main"));
let content = serde_json::from_value(serde_json::json!({
"type": "text",
"text": "Say PONG",
}))
.expect("content block");
let result = os
.prompt(PromptInput {
session_id: None,
idempotency_key: Some(String::from("prompt-1")),
content: vec![content],
})
.await
.expect("durable prompt");
assert_eq!(result.session_id, "main");
assert!(serde_json::to_string(&result.message)
.expect("serialize prompt message")
.contains(MOCK_PROMPT_TEXT));
let live = tokio::time::timeout(std::time::Duration::from_secs(5), events.next())
.await
.expect("live event timeout")
.expect("live event stream")
.expect("live event lagged");
assert!(matches!(live, SessionStreamEntry::Durable(_)));
let history = os
.read_history(ReadHistoryInput::default())
.await
.expect("history");
assert_eq!(history.events.len(), 2, "user and completed agent messages");
assert_eq!(history.events[0].sequence, 1);
assert_eq!(history.events[1].sequence, 2);
os.unload_session(None).await.expect("unload");
assert_eq!(
os.get_session(None)
.await
.expect("SQLite get")
.latest_sequence,
2
);
os.delete_session(None).await.expect("delete");
assert!(os.get_session(None).await.is_err());
os.shutdown().await.expect("shutdown");
std::fs::remove_dir_all(&package_root).ok();
}