Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f9d7252
test(conformance): port sandbox lifecycle and API e2e tests to testkit
matthewgrossman Nov 9, 2026
e6d26d4
test(testkit): add CLI sandbox, exec success, details, wait_absent an…
matthewgrossman Nov 9, 2026
2566437
test(conformance): use ExecOutput::success for exec checks
matthewgrossman Nov 9, 2026
ff1dbe3
test(testkit): add create_ready, expect_stdout, poll_exec and deadlin…
matthewgrossman Nov 10, 2026
db57da5
test(server): drive the WebSocket tunnel test through the SDK tunnel …
matthewgrossman Nov 10, 2026
58c18c0
test(gateway): build the preflight report once
matthewgrossman Nov 10, 2026
eed441d
refactor(testkit): make the sandbox and output helpers uniform
matthewgrossman Nov 10, 2026
9ec4943
refactor(testkit): reuse YamlFile and report stdout when CLI checks fail
matthewgrossman Nov 10, 2026
ecb4cc8
refactor(testkit): use the new sandbox helpers with the credential ga…
matthewgrossman Nov 10, 2026
b415e62
test(gateway): compile the proxy preflight tests only with a proxy-ta…
matthewgrossman Nov 10, 2026
e4c5d93
test(conformance): wait for labeled sandboxes and allow a 60s on-fail…
matthewgrossman Nov 10, 2026
b6ca650
refactor(testkit): share sandbox handles and output checks across bui…
matthewgrossman Nov 10, 2026
1cc2fb5
refactor(testkit): tighten error helpers and document test scope
matthewgrossman Nov 10, 2026
db562a7
test(e2e): keep edge tunnel and ephemeral cleanup e2e until their ports
matthewgrossman Nov 10, 2026
d9eb4ab
chore(tasks): drop the live-Internet perf benchmark port
matthewgrossman Nov 10, 2026
1975b13
test(e2e): restore the docker proxy settings insertion unit test
matthewgrossman Nov 10, 2026
be08bd4
test(conformance): ignore the on-failure restart test until #5507
matthewgrossman Nov 12, 2026
d39b42c
test(e2e): keep the on-failure restart and global settings tests in e…
elezar Nov 12, 2026
68ccef8
test(conformance): only read the global setting in the settings round…
matthewgrossman Nov 12, 2026
36d882d
docs(conformance): point the global settings test at #5527
matthewgrossman Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ cargo test --manifest-path e2e/rust/Cargo.toml --features e2e --test sync
Run a single Docker-only test directly with cargo:

```shell
cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-docker --test docker_preflight
cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-docker --test docker_corporate_proxy
```

The harness (`e2e/rust/src/harness/`) provides:
Expand Down
142 changes: 142 additions & 0 deletions crates/openshell-cli/tests/doctor_integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! `openshell doctor check` against a fake `docker` on `PATH`, so the result
//! doesn't depend on the host's Docker.

#![cfg(unix)]

use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};

struct Doctor {
combined: String,
code: i32,
elapsed: Duration,
}

/// Run `openshell doctor check` with only `bin` on `PATH`.
fn doctor_check(bin: &Path) -> Doctor {
let home = tempfile::tempdir().expect("create isolated config dir");
let start = Instant::now();
let output = Command::new(env!("CARGO_BIN_EXE_openshell"))
.args(["doctor", "check"])
.env("XDG_CONFIG_HOME", home.path())
.env("HOME", home.path())
.env("PATH", bin)
.env("NO_COLOR", "1")
.env_remove("DOCKER_HOST")
.env_remove("OPENSHELL_GATEWAY")
.env_remove("OPENSHELL_GATEWAY_ENDPOINT")
.stdin(Stdio::null())
.output()
.expect("run openshell doctor check");
Doctor {
combined: format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
code: output.status.code().unwrap_or(-1),
elapsed: start.elapsed(),
}
}

/// Write an executable `docker` script to `bin` that records its arguments
/// in `bin/docker-args` and then runs `body`.
fn fake_docker(bin: &Path, body: &str) {
let docker = bin.join("docker");
let args = bin.join("docker-args");
fs::write(
&docker,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" > '{}'\n{body}\n",
args.display()
),
)
.expect("write fake docker");
fs::set_permissions(&docker, fs::Permissions::from_mode(0o755)).expect("chmod fake docker");
}

fn docker_args(bin: &Path) -> String {
fs::read_to_string(bin.join("docker-args")).unwrap_or_default()
}

#[test]
fn doctor_check_passes_when_docker_info_succeeds() {
let bin = tempfile::tempdir().expect("create fake bin dir");
fake_docker(bin.path(), "echo 27.3.1");

let doctor = doctor_check(bin.path());
assert_eq!(doctor.code, 0, "doctor check failed:\n{}", doctor.combined);
assert_eq!(
docker_args(bin.path()).trim(),
"info --format {{.ServerVersion}}",
"doctor check ran docker with unexpected arguments"
);
for expected in ["Docker", "ok (version 27.3.1)", "All checks passed"] {
assert!(
doctor.combined.contains(expected),
"expected {expected:?} in doctor check output:\n{}",
doctor.combined
);
}
}

#[test]
fn doctor_check_fails_fast_when_docker_info_fails() {
let bin = tempfile::tempdir().expect("create fake bin dir");
fake_docker(
bin.path(),
"echo 'Cannot connect to the Docker daemon' >&2\nexit 1",
);

let doctor = doctor_check(bin.path());
assert_ne!(
doctor.code, 0,
"doctor check passed although docker info failed:\n{}",
doctor.combined
);
assert!(
doctor.elapsed < Duration::from_secs(10),
"doctor check took {:?}",
doctor.elapsed
);
for expected in [
"Docker",
"FAILED",
"docker info failed",
"Cannot connect to the Docker daemon",
] {
assert!(
doctor.combined.contains(expected),
"expected {expected:?} in doctor check output:\n{}",
doctor.combined
);
}
assert!(
!doctor.combined.contains("All checks passed"),
"doctor check reported success:\n{}",
doctor.combined
);
}

#[test]
fn doctor_check_fails_without_docker_on_path() {
let bin = tempfile::tempdir().expect("create empty bin dir");

let doctor = doctor_check(bin.path());
assert_ne!(
doctor.code, 0,
"doctor check passed without docker:\n{}",
doctor.combined
);
assert!(
doctor.combined.contains("failed to execute docker info"),
"doctor check did not report that docker could not run:\n{}",
doctor.combined
);
}
136 changes: 136 additions & 0 deletions crates/openshell-gateway/tests/proxy_config_preflight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! An incoherent driver proxy table is fatal, rather than leaving every dial
//! direct while looking configured. A bypass list without a proxy URL means
//! the operator believed proxying was in effect.
// Only Docker, Podman and the VM driver have a proxy table.
#![cfg(all(
unix,
any(
feature = "compute-driver-docker",
feature = "compute-driver-podman",
feature = "compute-driver-vm"
)
))]

use std::fs;
use std::path::PathBuf;
use std::process::Output;
use std::time::Duration;

/// Drivers with an upstream proxy table, compiled into this gateway.
const DRIVERS: &[&str] = &[
#[cfg(feature = "compute-driver-docker")]
"docker",
#[cfg(feature = "compute-driver-podman")]
"podman",
#[cfg(feature = "compute-driver-vm")]
"vm",
];

struct Fixture {
root: tempfile::TempDir,
config: PathBuf,
}

impl Fixture {
/// A gateway config that selects `driver` and sets only `no_proxy` in its
/// table.
fn incoherent(driver: &str) -> Self {
let root = tempfile::tempdir().expect("fixture root");
let config = root.path().join("gateway.toml");
fs::write(
&config,
format!(
"[openshell]
version = 2
[openshell.gateway]
disable_tls = true
bind_address = \"127.0.0.1:0\"
compute_driver = {driver:?}
[openshell.gateway.auth]
allow_unauthenticated_users = true
[openshell.drivers.{driver}]
no_proxy = \"10.0.0.0/8\"
"
),
)
.expect("gateway configuration");
Self { root, config }
}

async fn run(&self, args: &[&str]) -> Output {
let mut command = tokio::process::Command::new(env!("CARGO_BIN_EXE_openshell-gateway"));
command
.env_clear()
.env("HOME", self.root.path())
.env("PATH", "/usr/bin:/bin")
.env("NO_COLOR", "1")
.env("XDG_CONFIG_HOME", self.root.path().join("config-home"))
.env("XDG_STATE_HOME", self.root.path().join("state-home"))
.args(args)
.kill_on_drop(true);
tokio::time::timeout(Duration::from_secs(30), command.output())
.await
.expect("the gateway must exit instead of starting")
.expect("run openshell-gateway")
}

fn config_path(&self) -> &str {
self.config.to_str().expect("UTF-8 config path")
}
}

fn combined(output: &Output) -> String {
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}

#[tokio::test]
async fn preflight_rejects_no_proxy_without_proxy() {
for driver in DRIVERS {
let fixture = Fixture::incoherent(driver);
let original = fs::read(&fixture.config).expect("original config");
let output = fixture
.run(&["config", "preflight", "--path", fixture.config_path()])
.await;
let report = combined(&output);
assert!(
!output.status.success(),
"preflight accepted an incoherent [openshell.drivers.{driver}] proxy table:\n{report}"
);
assert!(
report.contains("category=malformed"),
"preflight did not classify the {driver} config as malformed:\n{report}"
);
assert_eq!(
fs::read(&fixture.config).expect("config after preflight"),
original,
"preflight changed the {driver} config"
);
}
}

#[tokio::test]
async fn startup_rejects_no_proxy_without_proxy_and_names_the_keys() {
for driver in DRIVERS {
let fixture = Fixture::incoherent(driver);
let output = fixture.run(&["--config", fixture.config_path()]).await;
let report = combined(&output);
assert!(
!output.status.success(),
"the gateway started with an incoherent [openshell.drivers.{driver}] proxy table:\n{report}"
);
// Matched on the key names, not the wording, so error wrapping can
// change.
assert!(
report.contains("no_proxy") && report.contains("https_proxy"),
"the {driver} startup error does not name the offending keys:\n{report}"
);
}
}
19 changes: 0 additions & 19 deletions e2e/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,6 @@ name = "vm_bind_mount"
path = "tests/vm_bind_mount.rs"
required-features = ["e2e-vm"]

[[test]]
name = "docker_preflight"
path = "tests/docker_preflight.rs"
required-features = ["e2e-docker"]

[[test]]
name = "docker_corporate_proxy"
path = "tests/docker_corporate_proxy.rs"
Expand Down Expand Up @@ -144,11 +139,6 @@ name = "podman_corporate_proxy"
path = "tests/podman_corporate_proxy.rs"
required-features = ["e2e-podman"]

[[test]]
name = "podman_oci_identity"
path = "tests/podman_oci_identity.rs"
required-features = ["e2e-podman"]

[[test]]
name = "podman_resource_limits"
path = "tests/podman_resource_limits.rs"
Expand Down Expand Up @@ -234,11 +224,6 @@ name = "workspace_lifecycle"
path = "tests/workspace_lifecycle.rs"
required-features = ["e2e"]

[[test]]
name = "sandbox_templates"
path = "tests/sandbox_templates.rs"
required-features = ["e2e"]

[[test]]
name = "supervisor_middleware_http_hooks"
path = "tests/supervisor_middleware_http_hooks.rs"
Expand Down Expand Up @@ -316,7 +301,3 @@ module_name_repetitions = "allow"
must_use_candidate = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
[[test]]
name = "live_internet_traffic_perf"
path = "tests/live_internet_traffic_perf.rs"
required-features = ["e2e-host-gateway"]
5 changes: 0 additions & 5 deletions e2e/rust/e2e-podman.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,12 @@ source "${ROOT}/e2e/support/conformance.sh"
PODMAN_CI_TESTS=(
bypass_detection
ephemeral_cleanup
core_dump_hardening
default_image
driver_config_volume
edge_tunnel_e2e
forward_proxy_graphql_l7
forward_proxy_jsonrpc_l7
forward_proxy_l7_bypass
host_gateway_alias
landlock
live_policy_update
local_driver_token_restart
no_proxy
Expand All @@ -41,9 +38,7 @@ PODMAN_CI_TESTS=(
podman_corporate_proxy
podman_gateway_start
podman_host_gateway
podman_oci_identity
provider_token_exchange
upload_backpressure
)

if [ -n "${E2E_TEST}" ] && [ -n "${E2E_TEST_SET}" ]; then
Expand Down
16 changes: 1 addition & 15 deletions e2e/rust/src/harness/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,6 @@ impl SandboxGuard {
/// Returns an error if the CLI exits with a non-zero status or the sandbox
/// name cannot be parsed from the output.
pub async fn create(args: &[&str]) -> Result<Self, String> {
Self::create_inner(args, true).await
}

/// Create a sandbox using the gateway's configured default image.
///
/// Most E2E tests use [`Self::create`], which supplies the tool-capable E2E
/// image. This variant is reserved for coverage of the product default.
pub async fn create_with_gateway_default(args: &[&str]) -> Result<Self, String> {
Self::create_inner(args, false).await
}

async fn create_inner(args: &[&str], use_test_image: bool) -> Result<Self, String> {
let separator = args.iter().position(|arg| *arg == "--");
let (create_args, command) = separator.map_or((args, &[][..]), |index| {
(&args[..index], &args[index + 1..])
Expand All @@ -156,9 +144,7 @@ impl SandboxGuard {
let mut cmd = openshell_cmd();
cmd.arg("sandbox").arg("create").arg("--detach");
add_unique_name_if_missing(&mut cmd, create_args);
if use_test_image {
add_test_image_if_missing(&mut cmd, create_args);
}
add_test_image_if_missing(&mut cmd, create_args);
for arg in create_args {
cmd.arg(arg);
}
Expand Down
Loading
Loading