diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 80bbd58f7e..2c7608cbb0 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -209,11 +209,6 @@ name = "forward_proxy_jsonrpc_l7" path = "tests/forward_proxy_jsonrpc_l7.rs" required-features = ["e2e-host-gateway"] -[[test]] -name = "workspace_lifecycle" -path = "tests/workspace_lifecycle.rs" -required-features = ["e2e"] - [[test]] name = "sandbox_templates" path = "tests/sandbox_templates.rs" diff --git a/e2e/rust/tests/workspace_lifecycle.rs b/e2e/rust/tests/workspace_lifecycle.rs deleted file mode 100644 index 2fe946389e..0000000000 --- a/e2e/rust/tests/workspace_lifecycle.rs +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#![cfg(feature = "e2e")] - -//! E2E tests for workspace resource lifecycle. -//! -//! Covers: -//! - Workspace CRUD -//! - Provider creation scoped to a workspace -//! - Workspace isolation (resources in one workspace are invisible in another) -//! - `--all-workspaces` listing -//! - Deletion guard (workspace cannot be deleted while resources exist) -//! - Successful deletion after resource cleanup - -use std::process::Stdio; - -use openshell_e2e::harness::binary::{openshell_bin, openshell_cmd}; -use openshell_e2e::harness::output::strip_ansi; - -const WORKSPACE: &str = "lifecycle-test"; -const WORKSPACE_TERM: &str = "terminating-test"; -const PROVIDER: &str = "lifecycle-prov"; -const PROVIDER_TERM: &str = "term-prov"; - -struct CliResult { - output: String, - success: bool, -} - -async fn run_cli(args: &[&str]) -> CliResult { - let mut cmd = openshell_cmd(); - cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped()); - - let output = cmd.output().await.expect("spawn openshell command"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let combined = format!("{stdout}{stderr}"); - - CliResult { - output: strip_ansi(&combined), - success: output.status.success(), - } -} - -struct WorkspaceCleanup; - -impl Drop for WorkspaceCleanup { - fn drop(&mut self) { - let bin = openshell_bin(); - let _ = std::process::Command::new(&bin) - .args(["provider", "delete", PROVIDER, "--workspace", WORKSPACE]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - let _ = std::process::Command::new(&bin) - .args(["workspace", "delete", WORKSPACE]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - } -} - -#[tokio::test] -async fn workspace_full_crud_lifecycle() { - let _cleanup = WorkspaceCleanup; - - // 1. Create workspace. - let res = run_cli(&["workspace", "create", "--name", WORKSPACE]).await; - assert!(res.success, "workspace create failed: {}", res.output); - - // 2. Verify workspace exists. - let res = run_cli(&["workspace", "get", WORKSPACE]).await; - assert!(res.success, "workspace get failed: {}", res.output); - assert!( - res.output.contains(WORKSPACE), - "workspace get output should contain workspace name: {}", - res.output - ); - - // 3. Create provider in the workspace. - let res = run_cli(&[ - "provider", - "create", - "--name", - PROVIDER, - "--type", - "openai", - "--credential", - "OPENAI_API_KEY=test-value", - "--workspace", - WORKSPACE, - ]) - .await; - assert!( - res.success, - "provider create in workspace failed: {}", - res.output - ); - - // 4. List providers in workspace — should see our provider. - let res = run_cli(&["provider", "list", "--workspace", WORKSPACE]).await; - assert!(res.success, "provider list failed: {}", res.output); - assert!( - res.output.contains(PROVIDER), - "workspace-scoped list should show the provider: {}", - res.output - ); - - // 5. List providers in default workspace — should NOT see our provider. - let res = run_cli(&["provider", "list"]).await; - assert!( - res.success, - "provider list (default) failed: {}", - res.output - ); - assert!( - !res.output.contains(PROVIDER), - "default workspace should not contain the workspace-scoped provider: {}", - res.output - ); - - // 6. List providers with --all-workspaces — should see our provider. - let res = run_cli(&["provider", "list", "--all-workspaces"]).await; - assert!( - res.success, - "provider list --all-workspaces failed: {}", - res.output - ); - assert!( - res.output.contains(PROVIDER), - "--all-workspaces should include the workspace-scoped provider: {}", - res.output - ); - - // 7. Attempt workspace deletion — should fail because provider exists. - let res = run_cli(&["workspace", "delete", WORKSPACE]).await; - assert!( - !res.success, - "workspace delete should fail while resources exist: {}", - res.output - ); - assert!( - res.output.contains("still contains resources"), - "error should mention blocking resources: {}", - res.output - ); - - // 8. Delete the provider. - let res = run_cli(&["provider", "delete", PROVIDER, "--workspace", WORKSPACE]).await; - assert!(res.success, "provider delete failed: {}", res.output); - - // 9. Workspace deletion should now succeed. - let res = run_cli(&["workspace", "delete", WORKSPACE]).await; - assert!( - res.success, - "workspace delete should succeed after resources removed: {}", - res.output - ); - - // 10. Verify workspace is gone. - let res = run_cli(&["workspace", "get", WORKSPACE]).await; - assert!( - !res.success, - "workspace get should fail after deletion: {}", - res.output - ); -} - -struct TerminatingCleanup; - -impl Drop for TerminatingCleanup { - fn drop(&mut self) { - let bin = openshell_bin(); - let _ = std::process::Command::new(&bin) - .args([ - "provider", - "delete", - PROVIDER_TERM, - "--workspace", - WORKSPACE_TERM, - ]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - let _ = std::process::Command::new(&bin) - .args(["workspace", "delete", WORKSPACE_TERM]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - } -} - -#[tokio::test] -async fn workspace_terminating_rejects_creates() { - let _cleanup = TerminatingCleanup; - - // 1. Create workspace. - let res = run_cli(&["workspace", "create", "--name", WORKSPACE_TERM]).await; - assert!(res.success, "workspace create failed: {}", res.output); - - // 2. Create a provider to block deletion. - let res = run_cli(&[ - "provider", - "create", - "--name", - PROVIDER_TERM, - "--type", - "openai", - "--credential", - "OPENAI_API_KEY=test-value", - "--workspace", - WORKSPACE_TERM, - ]) - .await; - assert!(res.success, "provider create failed: {}", res.output); - - // 3. Attempt deletion — fails, but workspace is now Terminating. - let res = run_cli(&["workspace", "delete", WORKSPACE_TERM]).await; - assert!( - !res.success, - "delete should fail with blocker: {}", - res.output - ); - - // 4. Workspace list should show Terminating status. - let res = run_cli(&["workspace", "list"]).await; - assert!(res.success, "workspace list failed: {}", res.output); - assert!( - res.output.contains("Terminating"), - "workspace list should show Terminating status: {}", - res.output - ); - - // 5. Creating a new provider should be rejected. - let res = run_cli(&[ - "provider", - "create", - "--name", - "should-fail", - "--type", - "openai", - "--credential", - "OPENAI_API_KEY=test-value", - "--workspace", - WORKSPACE_TERM, - ]) - .await; - assert!( - !res.success, - "provider create should fail in terminating workspace: {}", - res.output - ); - assert!( - res.output.contains("being deleted"), - "error should mention workspace is being deleted: {}", - res.output - ); - - // 6. Delete the blocking provider (deletes still work in Terminating). - let res = run_cli(&[ - "provider", - "delete", - PROVIDER_TERM, - "--workspace", - WORKSPACE_TERM, - ]) - .await; - assert!(res.success, "provider delete failed: {}", res.output); - - // 7. Retry deletion — should succeed (idempotent on already-Terminating). - let res = run_cli(&["workspace", "delete", WORKSPACE_TERM]).await; - assert!( - res.success, - "workspace delete should succeed after cleanup: {}", - res.output - ); -} diff --git a/e2e/suites/conformance/README.md b/e2e/suites/conformance/README.md index e0ecece555..8a492223bc 100644 --- a/e2e/suites/conformance/README.md +++ b/e2e/suites/conformance/README.md @@ -10,7 +10,8 @@ conformance area and user story. The CLI is the current client interface; lifecy - `cli/tests//.rs`: test functions, steps, and assertions for a story. - `cli/tests//helpers.rs`: setup and assertions shared by that area's stories. -The current areas are `file_transfer`, `lifecycle`, `policy_advisor`, and `smoke`. +The current areas are `file_transfer`, `lifecycle`, `policy_advisor`, `smoke`, and +`workspaces`. Each area compiles into one test binary; individual stories remain selectable by their module prefix. `main.rs` contains only module declarations. @@ -66,7 +67,8 @@ must supply the workload image and tools used by the selected scenarios. Missing CLI or gateway prerequisites fail the run rather than silently passing. Run one group with `--test file_transfer`, `--test lifecycle`, -`--test policy_advisor`, or `--test smoke`, before the `--` separator. A group or +`--test policy_advisor`, `--test smoke`, or `--test workspaces`, before the `--` +separator. A group or individual-test filter exercises only part of the suite. The tmachine conformance testsuite runs these same entry points from the diff --git a/e2e/suites/conformance/cli/tests/workspaces/crud.rs b/e2e/suites/conformance/cli/tests/workspaces/crud.rs new file mode 100644 index 0000000000..57d1d22848 --- /dev/null +++ b/e2e/suites/conformance/cli/tests/workspaces/crud.rs @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::helpers::{fails, finish, require_status, succeeds}; +use openshell_e2e_support::OpenShellRunner; + +/// Verify a workspace can be created, inspected, listed as Active, deleted when +/// empty, and is gone afterwards. +#[tokio::test] +async fn workspace_full_crud_lifecycle() { + let mut runner = + OpenShellRunner::from_env("workspaces/crud").expect("candidate openshell CLI is available"); + let workspace = format!("ws-{}-cr", runner.id()); + let result = async { + runner.check_gateway_status().await?; + let r = &runner; + let name = workspace.as_str(); + succeeds( + r, + "create", + "workspace is created", + &["workspace", "create", "--name", name], + ) + .await?; + succeeds( + r, + "get", + "workspace can be retrieved", + &["workspace", "get", name], + ) + .await?; + require_status(r, "list-active", name, Some("Active")).await?; + succeeds( + r, + "delete", + "empty workspace is deleted", + &["workspace", "delete", name], + ) + .await?; + fails( + r, + "get-after-delete", + "deleted workspace is gone", + &["workspace", "get", name], + ) + .await?; + require_status(r, "list-absent", name, None).await + } + .await; + if let Err(error) = finish(runner, result, &workspace, &[]).await { + panic!("workspaces/crud conformance story failed:\n{error}"); + } +} diff --git a/e2e/suites/conformance/cli/tests/workspaces/helpers.rs b/e2e/suites/conformance/cli/tests/workspaces/helpers.rs new file mode 100644 index 0000000000..ad855ebbbd --- /dev/null +++ b/e2e/suites/conformance/cli/tests/workspaces/helpers.rs @@ -0,0 +1,215 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use openshell_e2e_support::{CommandResult, OpenShellRunner}; +use serde_json::Value; +use std::time::Duration; + +const COMMAND_TIMEOUT: Duration = Duration::from_mins(2); +const MAX_PAGES: u32 = 100; + +/// Run a CLI command and return its result without judging the outcome. +pub async fn run( + runner: &OpenShellRunner, + step: &str, + expectation: &str, + args: &[&str], +) -> Result { + runner + .step(step) + .description(expectation) + .with_timeout(COMMAND_TIMEOUT) + .run(args) + .await + .map_err(|error| error.to_string()) +} + +/// Run a CLI command that must succeed. +pub async fn succeeds( + runner: &OpenShellRunner, + step: &str, + expectation: &str, + args: &[&str], +) -> Result<(), String> { + run(runner, step, expectation, args) + .await? + .require_success() +} + +/// Run a CLI command that must fail. +pub async fn fails( + runner: &OpenShellRunner, + step: &str, + expectation: &str, + args: &[&str], +) -> Result<(), String> { + let result = run(runner, step, expectation, args).await?; + if result.success() { + return Err(result.failure_diagnostic(expectation)); + } + Ok(()) +} + +pub fn create_template_args<'a>(workspace: &'a str, name: &'a str) -> [&'a str; 6] { + [ + "sandbox", + "template", + "create", + name, + "--workspace", + workspace, + ] +} + +/// Find the entry called `name` in a paginated `--output json` listing, where +/// `collection` is the JSON key that holds the entries. +async fn find_entry( + runner: &OpenShellRunner, + step: &str, + list: &[&str], + collection: &str, + name: &str, +) -> Result, String> { + let mut token = String::new(); + for page in 0..MAX_PAGES { + let mut args = list.to_vec(); + args.extend(["--page-token", &token, "--output", "json"]); + let result = run(runner, &format!("{step}/{page}"), "list succeeds", &args).await?; + result.require_success()?; + let listing: Value = result.json().map_err(|error| error.to_string())?; + let Some(entries) = listing[collection].as_array() else { + return Err(format!("{step}: list output has no '{collection}' array")); + }; + if let Some(entry) = entries.iter().find(|entry| entry["name"] == name) { + return Ok(Some(entry.clone())); + } + token = listing["next_page_token"] + .as_str() + .unwrap_or_default() + .to_string(); + if token.is_empty() { + return Ok(None); + } + } + Err(format!( + "{step}: listing did not finish within {MAX_PAGES} pages" + )) +} + +/// Require the workspace's listed status to be `expected`, where `None` means +/// the workspace is not listed. +pub async fn require_status( + runner: &OpenShellRunner, + step: &str, + workspace: &str, + expected: Option<&str>, +) -> Result<(), String> { + let entry = find_entry( + runner, + step, + &["workspace", "list"], + "workspaces", + workspace, + ) + .await?; + let actual = entry.as_ref().and_then(|entry| entry["status"].as_str()); + if actual == expected { + return Ok(()); + } + Err(format!( + "{step}: workspace '{workspace}' is {actual:?}, expected {expected:?}" + )) +} + +/// Return the workspace that owns `template` in the listing selected by `scope` +/// (`--workspace `, `--all-workspaces`, or nothing for the default +/// workspace), or `None` when it is not listed. +pub async fn template_owner( + runner: &OpenShellRunner, + step: &str, + scope: &[&str], + template: &str, +) -> Result, String> { + let mut list = vec!["sandbox", "template", "list"]; + list.extend_from_slice(scope); + let entry = find_entry(runner, step, &list, "templates", template).await?; + Ok(entry.map(|entry| entry["workspace"].as_str().unwrap_or_default().to_string())) +} + +/// Delete a story's templates and workspace. Resources a successful story +/// already removed report "not found", which is fine; any other failure is +/// returned so cleanup problems stay visible. +async fn cleanup_workspace( + runner: &OpenShellRunner, + workspace: &str, + templates: &[&str], +) -> Result<(), String> { + let mut commands: Vec> = templates + .iter() + .map(|template| { + vec![ + "sandbox", + "template", + "delete", + template, + "--workspace", + workspace, + ] + }) + .collect(); + commands.push(vec!["workspace", "delete", workspace]); + let mut failures = Vec::new(); + for args in commands { + match run( + runner, + "cleanup", + "resource is deleted or already absent", + &args, + ) + .await + { + Ok(result) if result.success() || reports_not_found(&result) => {} + Ok(result) => failures.push(result.failure_diagnostic("cleanup delete succeeds")), + Err(error) => failures.push(error), + } + } + if failures.is_empty() { + Ok(()) + } else { + Err(failures.join("\n\n")) + } +} + +fn reports_not_found(result: &CommandResult) -> bool { + let output = format!("{}{}", result.stdout(), result.stderr()); + let output = output.to_ascii_lowercase(); + output.contains("sandbox template not found") + || output.contains("workspace not found") + || (output.contains("workspace '") && output.contains("' not found")) +} + +/// Combine a story result with its cleanup result, keeping both failures. +fn merge(result: Result<(), String>, cleanup: Result<(), String>) -> Result<(), String> { + match (result, cleanup) { + (Err(error), Err(cleanup)) => Err(format!("{error}\n\ncleanup also failed:\n{cleanup}")), + (Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error), + (Ok(()), Ok(())) => Ok(()), + } +} + +/// Finish a story. A passing story already removed its resources, so cleanup +/// runs only after a failure; both failures are kept. The runner then cleans up +/// its own tracked resources. +pub async fn finish( + runner: OpenShellRunner, + result: Result<(), String>, + workspace: &str, + templates: &[&str], +) -> Result<(), String> { + let cleanup = if result.is_err() { + cleanup_workspace(&runner, workspace, templates).await + } else { + Ok(()) + }; + runner.finish(merge(result, cleanup)).await +} diff --git a/e2e/suites/conformance/cli/tests/workspaces/main.rs b/e2e/suites/conformance/cli/tests/workspaces/main.rs new file mode 100644 index 0000000000..290cc5902b --- /dev/null +++ b/e2e/suites/conformance/cli/tests/workspaces/main.rs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Workspace conformance stories. +//! +//! Preconditions: a gateway that lets the test identity create and delete +//! workspaces and sandbox templates. Targets that restrict workspace creation, +//! such as Kubernetes operator-workspace mode, are not covered. Stories use +//! sandbox templates as the workspace-scoped resource because they need no +//! provider profile, credential or image from the target. +//! +//! Side effects: each story creates a workspace and templates named from the +//! run id, and removes them even when it fails. +//! +//! Known limitation: `workspace get` has no structured output, so stories check +//! its exit status and read workspace state from `workspace list --output json`. + +mod crud; +mod helpers; +mod resource_scoping; +mod terminating; diff --git a/e2e/suites/conformance/cli/tests/workspaces/resource_scoping.rs b/e2e/suites/conformance/cli/tests/workspaces/resource_scoping.rs new file mode 100644 index 0000000000..44f5a5c3b9 --- /dev/null +++ b/e2e/suites/conformance/cli/tests/workspaces/resource_scoping.rs @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::helpers::{create_template_args, finish, succeeds, template_owner}; +use openshell_e2e_support::OpenShellRunner; + +/// Verify a template created in a workspace is listed in that workspace and in +/// the all-workspaces view with its owner, and is not visible in the default +/// workspace. +#[tokio::test] +async fn template_is_scoped_to_its_workspace() { + let mut runner = OpenShellRunner::from_env("workspaces/resource-scoping") + .expect("candidate openshell CLI is available"); + let workspace = format!("ws-{}-sc", runner.id()); + let template = format!("ct-{}-sc", runner.id()); + let result = async { + runner.check_gateway_status().await?; + let r = &runner; + let (ws, tpl) = (workspace.as_str(), template.as_str()); + succeeds( + r, + "create", + "workspace is created", + &["workspace", "create", "--name", ws], + ) + .await?; + succeeds( + r, + "template-create", + "template is created in the workspace", + &create_template_args(ws, tpl), + ) + .await?; + + let scoped = template_owner(r, "list-scoped", &["--workspace", ws], tpl).await?; + let default = template_owner(r, "list-default", &[], tpl).await?; + let all = template_owner(r, "list-all", &["--all-workspaces"], tpl).await?; + if scoped.as_deref() != Some(ws) { + return Err(format!( + "{tpl} is owned by {scoped:?} in its workspace, not {ws}" + )); + } + if all.as_deref() != Some(ws) { + return Err(format!( + "{tpl} is owned by {all:?} across workspaces, not {ws}" + )); + } + if let Some(owner) = default { + return Err(format!( + "{tpl} is visible in the default workspace as {owner}" + )); + } + + succeeds( + r, + "template-delete", + "template is deleted", + &["sandbox", "template", "delete", tpl, "--workspace", ws], + ) + .await?; + succeeds( + r, + "delete", + "workspace is deleted once empty", + &["workspace", "delete", ws], + ) + .await + } + .await; + if let Err(error) = finish(runner, result, &workspace, &[&template]).await { + panic!("workspaces/resource-scoping conformance story failed:\n{error}"); + } +} diff --git a/e2e/suites/conformance/cli/tests/workspaces/terminating.rs b/e2e/suites/conformance/cli/tests/workspaces/terminating.rs new file mode 100644 index 0000000000..7eb677f2f4 --- /dev/null +++ b/e2e/suites/conformance/cli/tests/workspaces/terminating.rs @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::helpers::{ + create_template_args, fails, finish, require_status, succeeds, template_owner, +}; +use openshell_e2e_support::OpenShellRunner; + +/// Verify the deletion guard and the Terminating phase. Deleting a workspace +/// that still holds a resource is refused and leaves it Terminating; while it +/// is Terminating new resources are rejected and existing ones can still be +/// deleted; retrying the delete then succeeds. +/// +/// Known limitation: the CLI exposes no structured error, so rejections are +/// checked by exit status and by the resulting state, not by message. +#[tokio::test] +async fn workspace_terminating_rejects_creates() { + let mut runner = OpenShellRunner::from_env("workspaces/terminating") + .expect("candidate openshell CLI is available"); + let workspace = format!("ws-{}-tm", runner.id()); + let template = format!("ct-{}-tm", runner.id()); + let blocked = format!("ct-{}-bl", runner.id()); + let result = async { + runner.check_gateway_status().await?; + let r = &runner; + let (ws, tpl, bl) = (workspace.as_str(), template.as_str(), blocked.as_str()); + succeeds( + r, + "create", + "workspace is created", + &["workspace", "create", "--name", ws], + ) + .await?; + succeeds( + r, + "template-create", + "template is created in the workspace", + &create_template_args(ws, tpl), + ) + .await?; + + succeeds( + r, + "control-create", + "the same template can be created while the workspace is Active", + &create_template_args(ws, bl), + ) + .await?; + succeeds( + r, + "control-delete", + "and deleted again", + &["sandbox", "template", "delete", bl, "--workspace", ws], + ) + .await?; + + fails( + r, + "delete-blocked", + "delete is refused while the workspace holds a resource", + &["workspace", "delete", ws], + ) + .await?; + require_status(r, "list-terminating", ws, Some("Terminating")).await?; + + fails( + r, + "create-rejected", + "a new template is rejected while Terminating", + &create_template_args(ws, bl), + ) + .await?; + if template_owner(r, "list-rejected", &["--workspace", ws], bl) + .await? + .is_some() + { + return Err(format!( + "rejected template '{bl}' exists in workspace '{ws}'" + )); + } + + succeeds( + r, + "template-delete", + "an existing template can still be deleted", + &["sandbox", "template", "delete", tpl, "--workspace", ws], + ) + .await?; + succeeds( + r, + "delete-retry", + "retrying the delete succeeds once empty", + &["workspace", "delete", ws], + ) + .await?; + require_status(r, "list-absent", ws, None).await + } + .await; + if let Err(error) = finish(runner, result, &workspace, &[&template, &blocked]).await { + panic!("workspaces/terminating conformance story failed:\n{error}"); + } +} diff --git a/tests/artifacts.nix b/tests/artifacts.nix index 4e0921adb0..71e816909c 100644 --- a/tests/artifacts.nix +++ b/tests/artifacts.nix @@ -131,7 +131,6 @@ let # Needs a prebuilt musl DNS probe in guest artifact mode; tracked in #3009. "transparent_tcp" "websocket_conformance" - "workspace_lifecycle" ]; podmanE2eArchiveFilter = let