use std::path::{Path, PathBuf};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::model::filesystem::{FileSystem, StdFileSystem};
use crate::services::remote::{
build_kube_terminal_args, build_ssh_terminal_args, spawn_kube_reconnect_task,
spawn_reconnect_task, ConnectionParams, KubeConnection, KubeTarget, LocalLongRunningSpawner,
LocalProcessSpawner, LongRunningSpawner, ProcessSpawner, RemoteFileSystem,
RemoteLongRunningSpawner, RemoteProcessSpawner, SshConnection, SshError, TransportError,
};
use crate::services::workspace_trust::WorkspaceTrust;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathTranslationSpec {
pub host_root: String,
pub remote_root: String,
}
#[derive(Debug, Clone)]
pub struct PathTranslation {
pub host_root: PathBuf,
pub remote_root: PathBuf,
}
impl PathTranslation {
pub fn host_to_remote(&self, host: &Path) -> Option<PathBuf> {
let rel = host.strip_prefix(&self.host_root).ok()?;
Some(self.remote_root.join(rel))
}
pub fn remote_to_host(&self, remote: &Path) -> Option<PathBuf> {
let rel = remote.strip_prefix(&self.remote_root).ok()?;
Some(self.host_root.join(rel))
}
}
#[derive(Debug, Clone)]
pub struct TerminalWrapper {
pub command: String,
pub args: Vec<String>,
pub manages_cwd: bool,
}
impl TerminalWrapper {
pub fn host_shell() -> Self {
Self {
command: crate::services::terminal::manager::detect_shell(),
args: Vec::new(),
manages_cwd: false,
}
}
pub fn ssh(params: &ConnectionParams, remote_dir: Option<&str>) -> Self {
Self {
command: "ssh".to_string(),
args: build_ssh_terminal_args(params, remote_dir),
manages_cwd: true,
}
}
pub fn kube(target: &KubeTarget) -> Self {
Self {
command: "kubectl".to_string(),
args: build_kube_terminal_args(target),
manages_cwd: true,
}
}
pub fn with_user_shell_override(
mut self,
shell: Option<&crate::config::TerminalShellConfig>,
) -> Self {
if let Some(shell) = shell {
if !self.manages_cwd {
self.command = shell.command.clone();
self.args = shell.args.clone();
}
}
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorityPayload {
pub filesystem: FilesystemSpec,
pub spawner: SpawnerSpec,
pub terminal_wrapper: TerminalWrapperSpec,
#[serde(default)]
pub display_label: String,
#[serde(default)]
pub path_translation: Option<PathTranslationSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum FilesystemSpec {
Local,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum SpawnerSpec {
Local,
DockerExec {
container_id: String,
#[serde(default)]
user: Option<String>,
#[serde(default)]
workspace: Option<String>,
#[serde(default)]
env: Vec<(String, String)>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum TerminalWrapperSpec {
HostShell,
Explicit {
command: String,
args: Vec<String>,
#[serde(default = "default_true")]
manages_cwd: bool,
},
}
fn default_true() -> bool {
true
}
#[derive(Clone)]
pub struct Authority {
pub filesystem: Arc<dyn FileSystem + Send + Sync>,
pub process_spawner: Arc<dyn ProcessSpawner>,
pub long_running_spawner: Arc<dyn LongRunningSpawner>,
pub terminal_wrapper: TerminalWrapper,
pub display_label: String,
pub path_translation: Option<PathTranslation>,
pub workspace_trust: Arc<WorkspaceTrust>,
pub env_provider: Arc<crate::services::env_provider::EnvProvider>,
}
impl Authority {
pub fn local(
trust: Arc<WorkspaceTrust>,
env: Arc<crate::services::env_provider::EnvProvider>,
) -> Self {
Self {
filesystem: Arc::new(StdFileSystem),
process_spawner: Arc::new(LocalProcessSpawner::new(
Arc::clone(&env),
Arc::clone(&trust),
)),
long_running_spawner: Arc::new(LocalLongRunningSpawner::new(
Arc::clone(&env),
Arc::clone(&trust),
)),
terminal_wrapper: TerminalWrapper::host_shell(),
display_label: String::new(),
path_translation: None,
workspace_trust: trust,
env_provider: env,
}
}
pub fn ssh(
filesystem: Arc<dyn FileSystem + Send + Sync>,
process_spawner: Arc<dyn ProcessSpawner>,
long_running_spawner: Arc<dyn LongRunningSpawner>,
params: &ConnectionParams,
remote_dir: Option<&str>,
trust: Arc<WorkspaceTrust>,
env: Arc<crate::services::env_provider::EnvProvider>,
) -> Self {
Self {
filesystem,
process_spawner,
long_running_spawner,
terminal_wrapper: TerminalWrapper::ssh(params, remote_dir),
display_label: String::new(),
path_translation: None,
workspace_trust: trust,
env_provider: env,
}
}
pub fn kube(
filesystem: Arc<dyn FileSystem + Send + Sync>,
process_spawner: Arc<dyn ProcessSpawner>,
long_running_spawner: Arc<dyn LongRunningSpawner>,
target: &KubeTarget,
trust: Arc<WorkspaceTrust>,
env: Arc<crate::services::env_provider::EnvProvider>,
) -> Self {
Self {
filesystem,
process_spawner,
long_running_spawner,
terminal_wrapper: TerminalWrapper::kube(target),
display_label: target.display(),
path_translation: None,
workspace_trust: trust,
env_provider: env,
}
}
pub fn kube_from_connection(
connection: &KubeConnection,
target: KubeTarget,
base_env: Vec<(String, String)>,
trust: Arc<WorkspaceTrust>,
env: Arc<crate::services::env_provider::EnvProvider>,
) -> Self {
let channel = connection.channel();
let filesystem: Arc<dyn FileSystem + Send + Sync> = Arc::new(RemoteFileSystem::new(
channel.clone(),
connection.connection_string().to_string(),
));
let process_spawner: Arc<dyn ProcessSpawner> = Arc::new(RemoteProcessSpawner::new(
channel,
Arc::clone(&env),
Arc::clone(&trust),
));
let long_running_spawner: Arc<dyn LongRunningSpawner> = Arc::new(
KubectlLongRunningSpawner::with_env(target.clone(), base_env, Arc::clone(&trust)),
);
Self::kube(
filesystem,
process_spawner,
long_running_spawner,
&target,
trust,
env,
)
}
pub fn from_plugin_payload(
payload: AuthorityPayload,
trust: Arc<WorkspaceTrust>,
env: Arc<crate::services::env_provider::EnvProvider>,
) -> Result<Self, AuthorityPayloadError> {
let filesystem: Arc<dyn FileSystem + Send + Sync> = match payload.filesystem {
FilesystemSpec::Local => Arc::new(StdFileSystem),
};
let (process_spawner, long_running_spawner): (
Arc<dyn ProcessSpawner>,
Arc<dyn LongRunningSpawner>,
) = match payload.spawner {
SpawnerSpec::Local => (
Arc::new(LocalProcessSpawner::new(
Arc::clone(&env),
Arc::clone(&trust),
)),
Arc::new(LocalLongRunningSpawner::new(
Arc::clone(&env),
Arc::clone(&trust),
)),
),
SpawnerSpec::DockerExec {
container_id,
user,
workspace,
env,
} => (
Arc::new(
crate::services::authority::docker_spawner::DockerExecSpawner::with_env(
container_id.clone(),
user.clone(),
workspace.clone(),
env.clone(),
Arc::clone(&trust),
),
),
Arc::new(
crate::services::authority::docker_spawner::DockerLongRunningSpawner::with_env(
container_id,
user,
workspace,
env,
Arc::clone(&trust),
),
),
),
};
let terminal_wrapper = match payload.terminal_wrapper {
TerminalWrapperSpec::HostShell => TerminalWrapper::host_shell(),
TerminalWrapperSpec::Explicit {
command,
args,
manages_cwd,
} => TerminalWrapper {
command,
args,
manages_cwd,
},
};
let path_translation = payload.path_translation.map(|spec| PathTranslation {
host_root: PathBuf::from(spec.host_root),
remote_root: PathBuf::from(spec.remote_root),
});
Ok(Self {
filesystem,
process_spawner,
long_running_spawner,
terminal_wrapper,
display_label: payload.display_label,
path_translation,
workspace_trust: trust,
env_provider: env,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteAgentSpec {
pub transport: RemoteTransportSpec,
#[serde(default)]
pub base_env: Vec<(String, String)>,
#[serde(default)]
pub window: bool,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub command: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum RemoteTransportSpec {
KubectlExec {
#[serde(default)]
context: Option<String>,
namespace: String,
pod: String,
#[serde(default)]
container: Option<String>,
#[serde(default)]
workspace: Option<String>,
},
Ssh {
#[serde(default)]
user: Option<String>,
host: String,
#[serde(default)]
port: Option<u16>,
#[serde(default)]
identity_file: Option<String>,
#[serde(default)]
remote_path: Option<String>,
#[serde(default)]
extra_args: Vec<String>,
},
}
impl RemoteAgentSpec {
pub fn into_kube_target(self) -> (KubeTarget, Vec<(String, String)>) {
match self.transport {
RemoteTransportSpec::KubectlExec {
context,
namespace,
pod,
container,
workspace,
} => (
KubeTarget {
context,
namespace,
pod,
container,
workspace,
},
self.base_env,
),
RemoteTransportSpec::Ssh { .. } => {
unreachable!("into_kube_target called on a non-kube transport")
}
}
}
}
pub struct KubeKeepalive {
reconnect: tokio::task::JoinHandle<()>,
_connection: KubeConnection,
_runtime: tokio::runtime::Runtime,
}
impl Drop for KubeKeepalive {
fn drop(&mut self) {
self.reconnect.abort();
}
}
pub async fn connect_kube_authority(
target: KubeTarget,
base_env: Vec<(String, String)>,
trust: Arc<WorkspaceTrust>,
env: Arc<crate::services::env_provider::EnvProvider>,
cancel: Option<tokio::sync::oneshot::Receiver<()>>,
) -> Result<(Authority, KubeKeepalive), TransportError> {
type Built = Result<
(
KubeConnection,
tokio::task::JoinHandle<()>,
tokio::runtime::Runtime,
),
TransportError,
>;
let (tx, rx) = tokio::sync::oneshot::channel::<Built>();
let bootstrap_target = target.clone();
std::thread::Builder::new()
.name("kube-connect".to_string())
.spawn(move || {
let built: Built = (|| {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.thread_name("kube-agent")
.enable_all()
.build()
.map_err(|e| TransportError::AgentStartFailed(format!("runtime: {e}")))?;
let (connection, reconnect) = runtime.block_on(async {
let connection = match cancel {
Some(cancel) => tokio::select! {
biased;
_ = cancel => {
return Err(TransportError::AgentStartFailed(
"cancelled".to_string(),
));
}
res = KubeConnection::connect(bootstrap_target.clone()) => res?,
},
None => KubeConnection::connect(bootstrap_target.clone()).await?,
};
let reconnect =
spawn_kube_reconnect_task(&connection.channel(), bootstrap_target.clone());
Ok::<_, TransportError>((connection, reconnect))
})?;
Ok((connection, reconnect, runtime))
})();
#[allow(clippy::let_underscore_must_use)]
let _ = tx.send(built);
})
.map_err(|e| TransportError::AgentStartFailed(format!("connect thread: {e}")))?;
let (connection, reconnect, runtime) = rx
.await
.map_err(|_| TransportError::AgentStartFailed("connect thread vanished".to_string()))??;
let authority = Authority::kube_from_connection(&connection, target, base_env, trust, env);
Ok((
authority,
KubeKeepalive {
reconnect,
_connection: connection,
_runtime: runtime,
},
))
}
pub struct SshKeepalive {
reconnect: tokio::task::JoinHandle<()>,
_connection: SshConnection,
_runtime: tokio::runtime::Runtime,
}
impl Drop for SshKeepalive {
fn drop(&mut self) {
self.reconnect.abort();
}
}
pub async fn connect_ssh_authority(
params: ConnectionParams,
remote_dir: Option<String>,
trust: Arc<WorkspaceTrust>,
env: Arc<crate::services::env_provider::EnvProvider>,
cancel: Option<tokio::sync::oneshot::Receiver<()>>,
) -> Result<(Authority, SshKeepalive), SshError> {
type Built = Result<
(
SshConnection,
tokio::task::JoinHandle<()>,
tokio::runtime::Runtime,
),
SshError,
>;
let (tx, rx) = tokio::sync::oneshot::channel::<Built>();
let bootstrap_params = params.clone();
std::thread::Builder::new()
.name("ssh-connect".to_string())
.spawn(move || {
let built: Built = (|| {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.thread_name("ssh-agent")
.enable_all()
.build()
.map_err(|e| SshError::AgentStartFailed(format!("runtime: {e}")))?;
let (connection, reconnect) = runtime.block_on(async {
let connection = match cancel {
Some(cancel) => tokio::select! {
biased;
_ = cancel => {
return Err(SshError::AgentStartFailed("cancelled".to_string()));
}
res = SshConnection::connect(bootstrap_params.clone()) => res?,
},
None => SshConnection::connect(bootstrap_params.clone()).await?,
};
let reconnect =
spawn_reconnect_task(connection.channel(), connection.params().clone());
Ok::<_, SshError>((connection, reconnect))
})?;
Ok((connection, reconnect, runtime))
})();
#[allow(clippy::let_underscore_must_use)]
let _ = tx.send(built);
})
.map_err(|e| SshError::AgentStartFailed(format!("connect thread: {e}")))?;
let (connection, reconnect, runtime) = rx
.await
.map_err(|_| SshError::AgentStartFailed("connect thread vanished".to_string()))??;
let channel = connection.channel();
let connection_string = connection.connection_string().to_string();
let reconnect_params = connection.params().clone();
let filesystem: Arc<dyn FileSystem + Send + Sync> =
Arc::new(RemoteFileSystem::new(channel.clone(), connection_string));
let process_spawner: Arc<dyn ProcessSpawner> = Arc::new(RemoteProcessSpawner::new(
channel.clone(),
Arc::clone(&env),
Arc::clone(&trust),
));
let long_running_spawner: Arc<dyn LongRunningSpawner> =
Arc::new(RemoteLongRunningSpawner::new(
reconnect_params.clone(),
Arc::clone(&env),
Arc::clone(&trust),
));
let authority = Authority::ssh(
filesystem,
process_spawner,
long_running_spawner,
&reconnect_params,
remote_dir.as_deref(),
trust,
env,
);
Ok((
authority,
SshKeepalive {
reconnect,
_connection: connection,
_runtime: runtime,
},
))
}
#[derive(Debug, thiserror::Error)]
pub enum AuthorityPayloadError {
#[error("invalid authority payload: {0}")]
Invalid(String),
}
mod docker_spawner;
mod kube_spawner;
pub(crate) use kube_spawner::KubectlLongRunningSpawner;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_authority_uses_host_shell_with_no_args() {
let auth = Authority::local(
Arc::new(WorkspaceTrust::permissive()),
Arc::new(crate::services::env_provider::EnvProvider::inactive()),
);
assert!(!auth.terminal_wrapper.command.is_empty());
assert!(auth.terminal_wrapper.args.is_empty());
assert!(!auth.terminal_wrapper.manages_cwd);
assert_eq!(auth.display_label, "");
}
#[test]
fn kube_terminal_wrapper_reparents_into_pod() {
let target = KubeTarget {
context: Some("prod".into()),
namespace: "dev".into(),
pod: "pod-1".into(),
container: None,
workspace: Some("/workspace".into()),
};
let wrapper = TerminalWrapper::kube(&target);
assert_eq!(wrapper.command, "kubectl");
assert!(wrapper.manages_cwd);
assert_eq!(wrapper.args[0], "--context");
assert!(wrapper.args.iter().any(|a| a == "-it"));
assert!(wrapper.args.iter().any(|a| a == "pod-1"));
let override_shell = crate::config::TerminalShellConfig {
command: "/usr/local/bin/fish".into(),
args: vec![],
};
let after = wrapper
.clone()
.with_user_shell_override(Some(&override_shell));
assert_eq!(after.command, "kubectl");
}
#[test]
fn remote_agent_spec_parses_plugin_payload() {
let json = serde_json::json!({
"transport": {
"kind": "kubectl-exec",
"context": "k3d-dev",
"namespace": "dev",
"pod": "fresh-7c9f",
"container": "app",
"workspace": "/workspace"
},
"base_env": [
["PATH", "/home/dev/.local/bin:/usr/bin"],
["LANG", "C.UTF-8"]
]
});
let spec: RemoteAgentSpec = serde_json::from_value(json).expect("spec parses");
let (target, base_env) = spec.into_kube_target();
assert_eq!(target.context.as_deref(), Some("k3d-dev"));
assert_eq!(target.namespace, "dev");
assert_eq!(target.pod, "fresh-7c9f");
assert_eq!(target.container.as_deref(), Some("app"));
assert_eq!(target.workspace.as_deref(), Some("/workspace"));
assert_eq!(base_env.len(), 2);
assert_eq!(
base_env[0],
(
"PATH".to_string(),
"/home/dev/.local/bin:/usr/bin".to_string()
)
);
let minimal = serde_json::json!({
"transport": { "kind": "kubectl-exec", "namespace": "dev", "pod": "p" }
});
let spec2: RemoteAgentSpec = serde_json::from_value(minimal).expect("minimal parses");
let (t2, env2) = spec2.into_kube_target();
assert!(t2.context.is_none() && t2.container.is_none() && t2.workspace.is_none());
assert!(env2.is_empty());
}
#[test]
fn from_plugin_payload_local_yields_host_shell() {
let payload = AuthorityPayload {
filesystem: FilesystemSpec::Local,
spawner: SpawnerSpec::Local,
terminal_wrapper: TerminalWrapperSpec::HostShell,
display_label: String::new(),
path_translation: None,
};
let auth = Authority::from_plugin_payload(
payload,
Arc::new(WorkspaceTrust::permissive()),
Arc::new(crate::services::env_provider::EnvProvider::inactive()),
)
.expect("local payload is valid");
assert!(!auth.terminal_wrapper.command.is_empty());
assert!(auth.terminal_wrapper.args.is_empty());
}
#[test]
fn payload_roundtrips_through_serde_json() {
let json = serde_json::json!({
"filesystem": { "kind": "local" },
"spawner": {
"kind": "docker-exec",
"container_id": "abc123",
"user": "vscode",
"workspace": "/workspaces/proj"
},
"terminal_wrapper": {
"kind": "explicit",
"command": "docker",
"args": ["exec", "-it", "abc123", "bash", "-l"],
"manages_cwd": true
},
"display_label": "Container:abc123"
});
let payload: AuthorityPayload =
serde_json::from_value(json).expect("json matches payload schema");
let auth = Authority::from_plugin_payload(
payload,
Arc::new(WorkspaceTrust::permissive()),
Arc::new(crate::services::env_provider::EnvProvider::inactive()),
)
.expect("docker payload is valid");
assert_eq!(auth.terminal_wrapper.command, "docker");
assert!(auth.terminal_wrapper.manages_cwd);
assert_eq!(auth.display_label, "Container:abc123");
}
#[test]
fn payload_accepts_docker_exec_env_pairs() {
let json = serde_json::json!({
"filesystem": { "kind": "local" },
"spawner": {
"kind": "docker-exec",
"container_id": "abc123",
"user": "vscode",
"workspace": "/workspaces/proj",
"env": [
["PATH", "/home/vscode/.local/bin:/usr/bin"],
["LANG", "C.UTF-8"]
]
},
"terminal_wrapper": { "kind": "host-shell" }
});
let payload: AuthorityPayload =
serde_json::from_value(json).expect("env field is accepted");
if let SpawnerSpec::DockerExec { env, .. } = &payload.spawner {
assert_eq!(env.len(), 2);
assert_eq!(
env[0],
("PATH".into(), "/home/vscode/.local/bin:/usr/bin".into())
);
assert_eq!(env[1], ("LANG".into(), "C.UTF-8".into()));
} else {
panic!("expected docker-exec spawner");
}
let json_no_env = serde_json::json!({
"filesystem": { "kind": "local" },
"spawner": {
"kind": "docker-exec",
"container_id": "abc123"
},
"terminal_wrapper": { "kind": "host-shell" }
});
let payload2: AuthorityPayload =
serde_json::from_value(json_no_env).expect("env is optional");
if let SpawnerSpec::DockerExec { env, .. } = payload2.spawner {
assert!(env.is_empty());
} else {
panic!("expected docker-exec spawner");
}
}
#[test]
fn payload_defaults_manages_cwd_to_true_for_explicit_wrapper() {
let json = serde_json::json!({
"filesystem": { "kind": "local" },
"spawner": { "kind": "local" },
"terminal_wrapper": {
"kind": "explicit",
"command": "bash",
"args": []
}
});
let payload: AuthorityPayload =
serde_json::from_value(json).expect("manages_cwd is optional");
let auth = Authority::from_plugin_payload(
payload,
Arc::new(WorkspaceTrust::permissive()),
Arc::new(crate::services::env_provider::EnvProvider::inactive()),
)
.expect("payload is valid");
assert!(auth.terminal_wrapper.manages_cwd);
assert_eq!(auth.display_label, "");
}
#[test]
fn user_shell_override_replaces_host_shell_wrapper() {
let override_shell = crate::config::TerminalShellConfig {
command: "/usr/local/bin/fish".into(),
args: vec!["-l".into(), "-i".into()],
};
let wrapper = TerminalWrapper::host_shell().with_user_shell_override(Some(&override_shell));
assert_eq!(wrapper.command, "/usr/local/bin/fish");
assert_eq!(wrapper.args, vec!["-l".to_string(), "-i".to_string()]);
assert!(!wrapper.manages_cwd);
}
#[test]
fn user_shell_override_is_noop_when_wrapper_manages_cwd() {
let docker = TerminalWrapper {
command: "docker".into(),
args: vec![
"exec".into(),
"-w".into(),
"/workspaces/proj".into(),
"abc123".into(),
"bash".into(),
],
manages_cwd: true,
};
let override_shell = crate::config::TerminalShellConfig {
command: "/usr/local/bin/fish".into(),
args: vec![],
};
let wrapper = docker
.clone()
.with_user_shell_override(Some(&override_shell));
assert_eq!(wrapper.command, docker.command);
assert_eq!(wrapper.args, docker.args);
assert!(wrapper.manages_cwd);
}
#[test]
fn user_shell_override_none_leaves_wrapper_unchanged() {
let original = TerminalWrapper::host_shell();
let wrapper = original.clone().with_user_shell_override(None);
assert_eq!(wrapper.command, original.command);
assert_eq!(wrapper.args, original.args);
assert_eq!(wrapper.manages_cwd, original.manages_cwd);
}
#[test]
fn from_plugin_payload_docker_exec_carries_label() {
let payload = AuthorityPayload {
filesystem: FilesystemSpec::Local,
spawner: SpawnerSpec::DockerExec {
container_id: "abc123".into(),
user: Some("vscode".into()),
workspace: Some("/workspaces/proj".into()),
env: Vec::new(),
},
terminal_wrapper: TerminalWrapperSpec::Explicit {
command: "docker".into(),
args: vec![
"exec".into(),
"-it".into(),
"-u".into(),
"vscode".into(),
"-w".into(),
"/workspaces/proj".into(),
"abc123".into(),
"bash".into(),
"-l".into(),
],
manages_cwd: true,
},
display_label: "Container:abc123".into(),
path_translation: None,
};
let auth = Authority::from_plugin_payload(
payload,
Arc::new(WorkspaceTrust::permissive()),
Arc::new(crate::services::env_provider::EnvProvider::inactive()),
)
.expect("docker payload is valid");
assert_eq!(auth.terminal_wrapper.command, "docker");
assert!(auth.terminal_wrapper.manages_cwd);
assert_eq!(auth.display_label, "Container:abc123");
}
#[test]
fn path_translation_round_trips_under_workspace() {
let pt = PathTranslation {
host_root: PathBuf::from("/tmp/.tmpA1B2"),
remote_root: PathBuf::from("/workspaces/proj"),
};
let host = Path::new("/tmp/.tmpA1B2/src/util.py");
let remote = pt.host_to_remote(host).expect("host under host_root");
assert_eq!(remote, PathBuf::from("/workspaces/proj/src/util.py"));
assert_eq!(
pt.remote_to_host(&remote)
.expect("remote under remote_root"),
host.to_path_buf(),
);
}
#[test]
fn path_translation_returns_none_outside_root() {
let pt = PathTranslation {
host_root: PathBuf::from("/host/proj"),
remote_root: PathBuf::from("/workspaces/proj"),
};
assert!(pt
.host_to_remote(Path::new("/usr/include/stdio.h"))
.is_none());
assert!(pt
.remote_to_host(Path::new("/usr/include/stdio.h"))
.is_none());
}
#[test]
fn from_plugin_payload_with_path_translation_round_trips() {
let json = serde_json::json!({
"filesystem": { "kind": "local" },
"spawner": {
"kind": "docker-exec",
"container_id": "abc123",
"workspace": "/workspaces/proj"
},
"terminal_wrapper": { "kind": "host-shell" },
"path_translation": {
"host_root": "/tmp/.tmpA1B2",
"remote_root": "/workspaces/proj"
}
});
let payload: AuthorityPayload =
serde_json::from_value(json).expect("path_translation is accepted");
let auth = Authority::from_plugin_payload(
payload,
Arc::new(WorkspaceTrust::permissive()),
Arc::new(crate::services::env_provider::EnvProvider::inactive()),
)
.expect("payload with translation is valid");
let pt = auth
.path_translation
.expect("authority carries the translation");
assert_eq!(pt.host_root, PathBuf::from("/tmp/.tmpA1B2"));
assert_eq!(pt.remote_root, PathBuf::from("/workspaces/proj"));
}
}