#![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 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: {what}. Screen:\n{}",
harness.screen_to_string()
);
}
fn workspace_with_devcontainer(dc_json: &str) -> (tempfile::TempDir, PathBuf) {
fresh::i18n::set_locale("en");
let temp = tempfile::tempdir().unwrap();
let workspace = temp.path().canonicalize().unwrap();
let dc = workspace.join(".devcontainer");
fs::create_dir_all(&dc).unwrap();
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");
(temp, workspace)
}
fn attach(harness: &mut EditorTestHarness) {
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);
return;
}
if harness
.editor()
.authority()
.display_label
.starts_with("Container:")
{
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
harness.advance_time(std::time::Duration::from_millis(50));
}
panic!("attach never landed an authority");
}
fn drive_lifecycle_picker(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
bounded_wait(harness, "palette 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::Enter, KeyModifiers::NONE)
.unwrap();
}
fn run_post_create(harness: &mut EditorTestHarness, probe: &Path) -> String {
drive_lifecycle_picker(harness);
bounded_wait_for_file(harness, probe, std::time::Duration::from_secs(10));
fs::read_to_string(probe).unwrap_or_default()
}
fn bounded_wait_for_file(
harness: &mut EditorTestHarness,
path: &Path,
deadline: std::time::Duration,
) {
let start = std::time::Instant::now();
while start.elapsed() < deadline {
harness.tick_and_render().unwrap();
if path.exists() {
return;
}
std::thread::sleep(std::time::Duration::from_millis(25));
harness.advance_time(std::time::Duration::from_millis(25));
}
panic!(
"file {path:?} never appeared within {deadline:?}. Screen:\n{}",
harness.screen_to_string()
);
}
fn bounded_wait_for_probe_line(
harness: &mut EditorTestHarness,
path: &Path,
expected: impl Fn(&str) -> bool,
deadline: std::time::Duration,
) {
let start = std::time::Instant::now();
while start.elapsed() < deadline {
harness.tick_and_render().unwrap();
if path.exists() {
let content = fs::read_to_string(path).unwrap_or_default();
if content.lines().any(&expected) {
return;
}
}
std::thread::sleep(std::time::Duration::from_millis(25));
harness.advance_time(std::time::Duration::from_millis(25));
}
let content = fs::read_to_string(path).unwrap_or_default();
panic!(
"expected line never appeared in {path:?} within {deadline:?}. \
Probe contents:\n{content}\nScreen:\n{}",
harness.screen_to_string()
);
}
#[test]
fn lifecycle_object_form_must_run_in_parallel() {
let probe_temp = tempfile::tempdir().unwrap();
let start_a = probe_temp.path().join("start_a");
let start_b = probe_temp.path().join("start_b");
let start_c = probe_temp.path().join("start_c");
let done_a = probe_temp.path().join("done_a");
let done_b = probe_temp.path().join("done_b");
let done_c = probe_temp.path().join("done_c");
let barrier = |own_start: &Path, sib1: &Path, sib2: &Path, own_done: &Path| -> String {
format!(
r#"sh -c 'touch {own_start} && for i in $(seq 1 30); do if [ -f {sib1} ] && [ -f {sib2} ]; then touch {own_done}; exit 0; fi; sleep 0.1; done; exit 1'"#,
own_start = own_start.display(),
sib1 = sib1.display(),
sib2 = sib2.display(),
own_done = own_done.display(),
)
};
let cmd_a = barrier(&start_a, &start_b, &start_c, &done_a);
let cmd_b = barrier(&start_b, &start_a, &start_c, &done_b);
let cmd_c = barrier(&start_c, &start_a, &start_b, &done_c);
let dc_json = format!(
r#"{{
"name": "r1-parallel",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"postCreateCommand": {{
"a": {cmd_a},
"b": {cmd_b},
"done": {cmd_c}
}}
}}
"#,
cmd_a = serde_json::to_string(&cmd_a).unwrap(),
cmd_b = serde_json::to_string(&cmd_b).unwrap(),
cmd_c = serde_json::to_string(&cmd_c).unwrap(),
);
let (_w_temp, workspace) = workspace_with_devcontainer(&dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
drive_lifecycle_picker(&mut harness);
bounded_wait_for_file(&mut harness, &done_a, std::time::Duration::from_secs(10));
bounded_wait_for_file(&mut harness, &done_b, std::time::Duration::from_secs(10));
bounded_wait_for_file(&mut harness, &done_c, std::time::Duration::from_secs(10));
assert!(done_a.exists(), "entry `a` never satisfied the barrier");
assert!(done_b.exists(), "entry `b` never satisfied the barrier");
assert!(done_c.exists(), "entry `done` never satisfied the barrier");
}
#[test]
fn lifecycle_array_form_executes_verbatim() {
let probe_temp = tempfile::tempdir().unwrap();
let probe = probe_temp.path().join("g1.sentinel");
let dc_json = format!(
r#"{{
"name": "g1-array-form",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"postCreateCommand": ["sh", "-c", "touch {}"]
}}
"#,
probe.display(),
);
let (_w_temp, workspace) = workspace_with_devcontainer(&dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
let _ = run_post_create(&mut harness, &probe);
assert!(
probe.exists(),
"G1: array-form lifecycle command should execute via the \
array-branch in the plugin's lifecycle handler. Sentinel \
file at {probe:?} never appeared."
);
}
#[test]
fn no_user_means_no_dash_u_flag() {
let probe_temp = tempfile::tempdir().unwrap();
let probe = probe_temp.path().join("g2.log");
let dc_json = format!(
r#"{{
"name": "g2-no-user",
"image": "ubuntu:22.04",
"postCreateCommand": "echo USER_FLAG=${{FAKE_DC_USER-NONE}} > {}"
}}
"#,
probe.display(),
);
let (_w_temp, workspace) = workspace_with_devcontainer(&dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
let probe_text = run_post_create(&mut harness, &probe);
let line = probe_text.trim();
assert!(
line == "USER_FLAG=" || line == "USER_FLAG=NONE",
"G2: no remoteUser/containerUser should mean no `-u` flag. \
Probe: {line:?}"
);
}
#[test]
fn remote_user_defaults_to_container_user() {
let probe_temp = tempfile::tempdir().unwrap();
let probe = probe_temp.path().join("g3.log");
let dc_json = format!(
r#"{{
"name": "g3-fallback",
"image": "ubuntu:22.04",
"containerUser": "node",
"postCreateCommand": "echo USER=$FAKE_DC_USER >> {}"
}}
"#,
probe.display(),
);
let (_w_temp, workspace) = workspace_with_devcontainer(&dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
drive_lifecycle_picker(&mut harness);
bounded_wait_for_probe_line(
&mut harness,
&probe,
|l| l == "USER=node",
std::time::Duration::from_secs(10),
);
let probe_text = fs::read_to_string(&probe).unwrap_or_default();
assert!(
probe_text.lines().any(|l| l == "USER=node"),
"G3: with no remoteUser declared, spawner should pass \
`-u <containerUser>`. Probe: {probe_text:?}"
);
}
#[test]
fn jsonc_config_with_comments_and_trailing_commas_is_detected() {
let dc_json = r#"{
// Top-level comment.
"name": "g4-jsonc",
/* block comment
spanning lines */
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"forwardPorts": [8080,], // trailing comma in array
}
"#;
let (_w_temp, workspace) = workspace_with_devcontainer(dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
bounded_wait(&mut 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(&mut harness, "Reopen popup", |h| {
let s = h.screen_to_string();
s.contains("Dev Container Detected") && s.contains("Reopen in Container")
});
}
#[test]
fn subfolder_devcontainer_json_is_detected() {
fresh::i18n::set_locale("en");
let temp = tempfile::tempdir().unwrap();
let workspace = temp.path().canonicalize().unwrap();
let sub = workspace.join(".devcontainer").join("rust-dev");
fs::create_dir_all(&sub).unwrap();
fs::write(
sub.join("devcontainer.json"),
r#"{
"name": "g5-subfolder",
"image": "ubuntu:22.04",
"remoteUser": "vscode"
}
"#,
)
.unwrap();
let plugins_dir = workspace.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
copy_plugin(&plugins_dir, "devcontainer");
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
bounded_wait(&mut 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(&mut harness, "Reopen popup", |h| {
let s = h.screen_to_string();
s.contains("Dev Container Detected") && s.contains("Reopen in Container")
});
}
#[test]
fn lifecycle_object_form_must_run_all_entries_even_on_failure() {
let probe_temp = tempfile::tempdir().unwrap();
let b_sentinel = probe_temp.path().join("b.touched");
let dc_json = format!(
r#"{{
"name": "r2-fail-fast",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"postCreateCommand": {{
"a": "exit 1",
"b": "touch {b}"
}}
}}
"#,
b = b_sentinel.display(),
);
let (_w_temp, workspace) = workspace_with_devcontainer(&dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
bounded_wait(&mut harness, "palette open", |h| h.editor().is_prompting());
harness.type_text("Dev Container: Run Lifecycle").unwrap();
bounded_wait(&mut 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(
&mut harness,
"lifecycle picker shows postCreateCommand",
|h| h.screen_to_string().contains("postCreateCommand"),
);
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let max_iters = 400;
let mut found = false;
for _ in 0..max_iters {
harness.tick_and_render().unwrap();
let s = harness.screen_to_string();
if s.contains(" failed (exit ")
|| s.contains(" completed successfully")
|| b_sentinel.exists()
{
found = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(25));
harness.advance_time(std::time::Duration::from_millis(25));
}
if !found {
eprintln!(
"R2: picker outcome never showed up. Final screen:\n{}",
harness.screen_to_string()
);
}
harness.tick_and_render().unwrap();
assert!(
b_sentinel.exists(),
"R2 (failing on master): even when entry `a` exits 1, entry \
`b` must still run per spec. Sentinel {b_sentinel:?} missing."
);
}
#[test]
fn lifecycle_hooks_fire_in_spec_order_during_up() {
let probe_temp = tempfile::tempdir().unwrap();
let order = probe_temp.path().join("order.log");
let dc_json = format!(
r#"{{
"name": "r3-order",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"initializeCommand": "echo init >> {p}",
"onCreateCommand": "echo onCreate >> {p}",
"updateContentCommand":"echo updateContent >> {p}",
"postCreateCommand": "echo postCreate >> {p}",
"postStartCommand": "echo postStart >> {p}",
"postAttachCommand": "echo postAttach >> {p}"
}}
"#,
p = order.display(),
);
let (_w_temp, workspace) = workspace_with_devcontainer(&dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
bounded_wait_for_file(&mut harness, &order, std::time::Duration::from_secs(10));
bounded_wait(&mut harness, "postAttach line in order.log", |_| {
std::fs::read_to_string(&order)
.map(|s| s.contains("postAttach"))
.unwrap_or(false)
});
let raw = std::fs::read_to_string(&order).unwrap();
let lines: Vec<String> = raw
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
let expected = vec![
"init".to_string(),
"onCreate".to_string(),
"updateContent".to_string(),
"postCreate".to_string(),
"postStart".to_string(),
"postAttach".to_string(),
];
assert_eq!(
lines, expected,
"R3: lifecycle hooks must fire in spec order: \
init → onCreate → updateContent → postCreate → postStart → postAttach"
);
}
#[test]
fn forward_ports_host_port_string_renders_in_panel() {
let dc_json = r#"{
"name": "g6-host-port",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"forwardPorts": ["db:5432", 8080]
}
"#;
let (_w_temp, workspace) = workspace_with_devcontainer(dc_json);
let mut harness = EditorTestHarness::create(
180,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
bounded_wait(&mut harness, "palette open", |h| h.editor().is_prompting());
harness.type_text("Show Forwarded Ports").unwrap();
bounded_wait(&mut harness, "ports palette match", |h| {
h.screen_to_string()
.contains("Dev Container: Show Forwarded Ports")
});
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
bounded_wait(&mut harness, "ports panel renders", |h| {
h.screen_to_string().contains("Forwarded Ports")
});
let screen = harness.screen_to_string();
assert!(
screen.contains("db:5432"),
"G6: panel must render the host:port string `db:5432`. Screen:\n{screen}"
);
assert!(
screen.contains("8080"),
"G6: panel must still render the numeric port. Screen:\n{screen}"
);
}
#[test]
fn ports_attributes_on_auto_forward_renders_in_panel() {
let dc_json = r#"{
"name": "g7-auto-forward",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"forwardPorts": [3000, 9229],
"portsAttributes": {
"3000": { "label": "Web", "onAutoForward": "silent" },
"9229": { "label": "Debug", "onAutoForward": "notify" }
}
}
"#;
let (_w_temp, workspace) = workspace_with_devcontainer(dc_json);
let mut harness = EditorTestHarness::create(
180,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
bounded_wait(&mut harness, "palette open", |h| h.editor().is_prompting());
harness.type_text("Show Forwarded Ports").unwrap();
bounded_wait(&mut harness, "ports palette match", |h| {
h.screen_to_string()
.contains("Dev Container: Show Forwarded Ports")
});
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
bounded_wait(&mut harness, "ports panel renders", |h| {
h.screen_to_string().contains("Forwarded Ports")
});
let screen = harness.screen_to_string();
for (label, attr) in [("Web", "silent"), ("Debug", "notify")] {
let want = format!("{label} ({attr})");
assert!(
screen.contains(&want),
"G7: panel must render label + onAutoForward as `{want}`. Screen:\n{screen}"
);
}
}
#[test]
fn shutdown_action_stop_container_must_stop_on_detach() {
let dc_json = r#"{
"name": "b2-shutdown",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"shutdownAction": "stopContainer"
}
"#;
let (_w_temp, workspace) = workspace_with_devcontainer(dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
let label = harness.editor().authority().display_label.clone();
let container_id = label
.strip_prefix("Container:")
.expect("attached")
.to_string();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
bounded_wait(&mut harness, "palette open", |h| h.editor().is_prompting());
harness.type_text("Dev Container: Detach").unwrap();
bounded_wait(&mut harness, "detach palette match", |h| {
h.screen_to_string().contains("Dev Container: Detach")
});
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:"),
"Detach should clear the container authority. label = {:?}",
harness.editor().authority().display_label,
);
for _ in 0..20 {
harness.tick_and_render().unwrap();
std::thread::sleep(std::time::Duration::from_millis(25));
harness.advance_time(std::time::Duration::from_millis(25));
}
let state = harness
.fake_devcontainer_state()
.expect("fake-devcontainer enabled");
let status_path = state.join("containers").join(&container_id).join("status");
let status = std::fs::read_to_string(&status_path)
.unwrap_or_else(|e| panic!("status file missing at {status_path:?}: {e}"))
.trim()
.to_string();
assert_eq!(
status, "stopped",
"B2 (failing on master): shutdownAction \"stopContainer\" \
must stop the container on Detach. Today the plugin only \
clears the authority. Status: {status}"
);
}
#[test]
fn user_env_probe_must_apply_captured_env_to_lifecycle_commands() {
let probe_temp = tempfile::tempdir().unwrap();
let probed = probe_temp.path().join("probed.log");
let rc_path = probe_temp.path().join("user.rc");
std::fs::write(&rc_path, "export PROBED_VAR=fromProfile\n").unwrap();
let dc_json = format!(
r#"{{
"name": "b3-user-env-probe",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"userEnvProbe": "loginShell",
"remoteEnv": {{ "BASH_ENV": "{rc}" }},
"postCreateCommand": "echo PROBED=${{PROBED_VAR-unset}} >> {p}"
}}
"#,
rc = rc_path.display(),
p = probed.display(),
);
let (_w_temp, workspace) = workspace_with_devcontainer(&dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
let _ = run_post_create(&mut harness, &probed);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
let content = std::fs::read_to_string(&probed).unwrap_or_default();
if content.contains("PROBED=fromProfile") {
break;
}
harness.tick_and_render().unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
harness.advance_time(std::time::Duration::from_millis(50));
}
let content = std::fs::read_to_string(&probed).unwrap_or_default();
assert!(
content.contains("PROBED=fromProfile"),
"B3: userEnvProbe `loginShell` must capture the user shell's \
env and apply it to lifecycle commands. Probe content:\n{content}"
);
}
fn wait_for_file_path(path: &Path, deadline: std::time::Duration) -> bool {
let start = std::time::Instant::now();
while start.elapsed() < deadline {
if path.exists() {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
false
}
fn read_order_log(path: &Path) -> Vec<String> {
std::fs::read_to_string(path)
.map(|s| {
s.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect()
})
.unwrap_or_default()
}
#[test]
fn wait_for_default_blocks_up_at_update_content_command() {
let probe_temp = tempfile::tempdir().unwrap();
let order = probe_temp.path().join("order.log");
let dc_json = format!(
r#"{{
"name": "b1a-default-waitfor",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"initializeCommand": "echo init >> {p}",
"onCreateCommand": "echo onCreate >> {p}",
"updateContentCommand":"echo updateContent >> {p}",
"postCreateCommand": "sleep 1 && echo postCreate >> {p}",
"postStartCommand": "sleep 1.2 && echo postStart >> {p}",
"postAttachCommand": "sleep 1.4 && echo postAttach >> {p}"
}}
"#,
p = order.display(),
);
let (_w_temp, workspace) = workspace_with_devcontainer(&dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
assert!(
wait_for_file_path(&order, std::time::Duration::from_secs(3)),
"order.log should exist after attach (pre-waitFor hooks ran synchronously)"
);
let immediate = read_order_log(&order);
let expected_pre: Vec<String> = ["init", "onCreate", "updateContent"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(
immediate, expected_pre,
"B1a: with default waitFor=updateContentCommand, only \
pre-waitFor hooks should have run by the time `up` returns. \
Got: {immediate:?}"
);
let final_done_deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < final_done_deadline {
let lines = read_order_log(&order);
if lines.len() >= 6 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
let final_lines = read_order_log(&order);
assert_eq!(
final_lines,
vec![
"init".to_string(),
"onCreate".to_string(),
"updateContent".to_string(),
"postCreate".to_string(),
"postStart".to_string(),
"postAttach".to_string(),
],
"B1a: bg hooks must eventually all run, in spec order. Got: \
{final_lines:?}"
);
}
#[test]
fn wait_for_explicit_value_changes_the_cutoff() {
let probe_temp = tempfile::tempdir().unwrap();
let order = probe_temp.path().join("order.log");
let dc_json = format!(
r#"{{
"name": "b1b-explicit-waitfor",
"image": "ubuntu:22.04",
"remoteUser": "vscode",
"waitFor": "onCreateCommand",
"initializeCommand": "echo init >> {p}",
"onCreateCommand": "echo onCreate >> {p}",
"updateContentCommand":"sleep 1 && echo updateContent >> {p}",
"postCreateCommand": "sleep 1.2 && echo postCreate >> {p}"
}}
"#,
p = order.display(),
);
let (_w_temp, workspace) = workspace_with_devcontainer(&dc_json);
let mut harness = EditorTestHarness::create(
160,
40,
HarnessOptions::new()
.with_working_dir(workspace.clone())
.with_fake_devcontainer(),
)
.unwrap();
harness.tick_and_render().unwrap();
attach(&mut harness);
assert!(
wait_for_file_path(&order, std::time::Duration::from_secs(3)),
"order.log should exist after attach"
);
let immediate = read_order_log(&order);
assert_eq!(
immediate,
vec!["init".to_string(), "onCreate".to_string()],
"B1b: with waitFor=onCreateCommand, `updateContentCommand` and \
later must NOT have run when `up` returned. Got: {immediate:?}"
);
let final_done_deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < final_done_deadline {
let lines = read_order_log(&order);
if lines.len() >= 4 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
let final_lines = read_order_log(&order);
assert_eq!(
final_lines,
vec![
"init".to_string(),
"onCreate".to_string(),
"updateContent".to_string(),
"postCreate".to_string(),
],
"B1b: bg hooks must eventually all run, in spec order. Got: \
{final_lines:?}"
);
}