#![cfg(feature = "plugins")]
use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness, HarnessOptions};
use crossterm::event::{KeyCode, KeyModifiers};
use std::fs;
use std::path::{Path, PathBuf};
fn set_up_probe_workspace(
name: &str,
container_env: Option<(&str, &str)>,
remote_env: Option<(&str, &str)>,
) -> (tempfile::TempDir, PathBuf, PathBuf) {
fresh::i18n::set_locale("en");
let workspace_temp = tempfile::tempdir().unwrap();
let workspace = workspace_temp.path().canonicalize().unwrap();
let probe = workspace.join("probe.log");
let dc = workspace.join(".devcontainer");
fs::create_dir_all(&dc).unwrap();
let container_env_block = match container_env {
Some((k, v)) => format!(
r#" "containerEnv": {{ "{k}": "{v}" }},
"#
),
None => String::new(),
};
let remote_env_block = match remote_env {
Some((k, v)) => format!(
r#" "remoteEnv": {{ "{k}": "{v}" }},
"#
),
None => String::new(),
};
let dc_json = format!(
r#"{{
"name": "{name}",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
{container_env_block}{remote_env_block} "postCreateCommand": "{{ echo PWD=$PWD; echo REQUESTED_CWD=$FAKE_DC_REQUESTED_CWD; echo CE_TEST=${{CE_TEST-unset}}; echo RE_TEST=${{RE_TEST-unset}}; }} >> {probe} 2>&1"
}}
"#,
probe = probe.display(),
);
fs::write(dc.join("devcontainer.json"), dc_json).unwrap();
let plugins_dir = workspace.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
copy_plugin(&plugins_dir, "devcontainer");
(workspace_temp, workspace, probe)
}
fn bounded_wait<F>(harness: &mut EditorTestHarness, what: &str, mut cond: F)
where
F: FnMut(&EditorTestHarness) -> bool,
{
let max_iters = 200;
for _ in 0..max_iters {
harness.tick_and_render().unwrap();
if cond(harness) {
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
harness.advance_time(std::time::Duration::from_millis(50));
}
panic!(
"bounded_wait timed out waiting for {what}. Screen:\n{}",
harness.screen_to_string()
);
}
fn run_attach_and_postcreate(
harness: &mut EditorTestHarness,
probe: &Path,
expected_line: impl Fn(&str) -> bool,
) -> String {
bounded_wait(harness, "plugin command registration", |h| {
let reg = h.editor().command_registry().read().unwrap();
reg.get_all().iter().any(|c| c.name == "%cmd.run_lifecycle")
});
harness.editor().fire_plugins_loaded_hook();
bounded_wait(harness, "Reopen popup", |h| {
let s = h.screen_to_string();
s.contains("Dev Container Detected") && s.contains("Reopen in Container")
});
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let max_iters = 200;
for _ in 0..max_iters {
harness.tick_and_render().unwrap();
if let Some(auth) = harness.editor_mut().take_pending_authority() {
harness.editor_mut().set_boot_authority(auth);
break;
}
if harness
.editor()
.authority()
.display_label
.starts_with("Container:")
{
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
harness.advance_time(std::time::Duration::from_millis(50));
}
assert!(
harness
.editor()
.authority()
.display_label
.starts_with("Container:"),
"expected container authority after attach"
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
bounded_wait(harness, "palette prompt open", |h| {
h.editor().is_prompting()
});
harness.type_text("Dev Container: Run Lifecycle").unwrap();
bounded_wait(harness, "lifecycle palette match", |h| {
h.screen_to_string()
.contains("Dev Container: Run Lifecycle Command")
});
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
bounded_wait(harness, "lifecycle picker shows postCreateCommand", |h| {
h.screen_to_string().contains("postCreateCommand")
});
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap(); harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
bounded_wait(harness, "probe file has expected line", |_| {
let content = fs::read_to_string(probe).unwrap_or_default();
content.lines().any(&expected_line)
});
fs::read_to_string(probe).unwrap_or_default()
}
#[test]
fn lifecycle_command_cwd_must_be_remote_workspace_folder() {
let (_w_temp, workspace, probe) = set_up_probe_workspace("s1-cwd", None, None);
std::env::set_var("FAKE_DC_REMOTE_WORKSPACE", "/workspaces/s1-cwd-distinct");
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
let probe_text = run_attach_and_postcreate(&mut harness, &probe, |l| {
l == "REQUESTED_CWD=/workspaces/s1-cwd-distinct"
});
std::env::remove_var("FAKE_DC_REMOTE_WORKSPACE");
assert!(
probe_text
.lines()
.any(|l| l == "REQUESTED_CWD=/workspaces/s1-cwd-distinct"),
"S1 (failing on master): lifecycle commands should pass the in-container \
workspace as `-w` to docker exec; today they pass the host path. Probe:\n{probe_text}"
);
}
#[test]
fn lifecycle_command_must_see_remote_env() {
let (_w_temp, workspace, probe) =
set_up_probe_workspace("s2-remote-env", None, Some(("RE_TEST", "from-remoteEnv")));
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
let probe_text =
run_attach_and_postcreate(&mut harness, &probe, |l| l == "RE_TEST=from-remoteEnv");
assert!(
probe_text.lines().any(|l| l == "RE_TEST=from-remoteEnv"),
"S2 (failing on master): lifecycle commands should inherit \
`remoteEnv` per the spec; today the plugin never propagates it. \
Probe:\n{probe_text}"
);
}
#[test]
fn lifecycle_command_must_see_container_env() {
let (_w_temp, workspace, probe) = set_up_probe_workspace(
"s3-container-env",
Some(("CE_TEST", "from-containerEnv")),
None,
);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
let probe_text =
run_attach_and_postcreate(&mut harness, &probe, |l| l == "CE_TEST=from-containerEnv");
assert!(
probe_text.lines().any(|l| l == "CE_TEST=from-containerEnv"),
"S3 regression guard: fake docker exec replays containerEnv from \
`<state>/containers/<id>/container_env`. Probe:\n{probe_text}"
);
}