|
| 1 | +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +//! Sandbox lifecycle through the `openshell` CLI. |
| 5 | +//! |
| 6 | +//! Ported from the `smoke` and `sandbox-lifecycle` scenarios of the old |
| 7 | +//! `conformance/cli` suite. The other lifecycle tests act through the SDK; these |
| 8 | +//! check that the CLI commands for create, get, list, exec, stop, start, and |
| 9 | +//! delete work against the same gateway. |
| 10 | +
|
| 11 | +use serde_json::Value; |
| 12 | +use testkit::Context; |
| 13 | + |
| 14 | +const PAGE_SIZE: &str = "1000"; |
| 15 | +/// More pages than any test gateway should have; stops a repeated token from |
| 16 | +/// looping forever. |
| 17 | +const MAX_PAGES: usize = 1000; |
| 18 | + |
| 19 | +/// `openshell status` succeeds, and a sandbox created with the CLI is |
| 20 | +/// retrievable and Ready, appears in `sandbox list`, runs `sandbox exec`, and is |
| 21 | +/// gone from `sandbox list` after `sandbox delete`. |
| 22 | +#[testkit::test] |
| 23 | +async fn cli_create_exec_list_delete(cx: &mut Context) -> testkit::Result { |
| 24 | + cx.cli(["status"]).await?.success()?; |
| 25 | + |
| 26 | + let name = cx.name("cs"); |
| 27 | + cx.track(&name); |
| 28 | + cx.cli(["sandbox", "create", "--name", &name, "--detach"]) |
| 29 | + .await? |
| 30 | + .success()?; |
| 31 | + |
| 32 | + let sandbox = cx.adopt(&name).await?; |
| 33 | + let got: Value = cx |
| 34 | + .cli(["sandbox", "get", &name, "--output", "json"]) |
| 35 | + .await? |
| 36 | + .success()? |
| 37 | + .json()?; |
| 38 | + testkit::ensure!( |
| 39 | + got["name"] == name.as_str(), |
| 40 | + "sandbox get returned {:?}; expected {name:?}", |
| 41 | + got["name"] |
| 42 | + ); |
| 43 | + testkit::ensure!( |
| 44 | + got["phase"] == "Ready", |
| 45 | + "sandbox {name} is in phase {}; expected Ready", |
| 46 | + got["phase"] |
| 47 | + ); |
| 48 | + |
| 49 | + let listed = find_listed(cx, &name).await?; |
| 50 | + testkit::ensure!(listed.is_some(), "sandbox {name} is not in sandbox list"); |
| 51 | + |
| 52 | + let marker = format!("openshell-e2e-{}", cx.run_id()); |
| 53 | + let exec = cx |
| 54 | + .cli([ |
| 55 | + "sandbox", "exec", "--name", &name, "--no-tty", "--", "echo", &marker, |
| 56 | + ]) |
| 57 | + .await? |
| 58 | + .success()?; |
| 59 | + testkit::ensure!( |
| 60 | + exec.stdout == format!("{marker}\n"), |
| 61 | + "sandbox exec stdout was {:?}; expected {:?}", |
| 62 | + exec.stdout, |
| 63 | + format!("{marker}\n") |
| 64 | + ); |
| 65 | + |
| 66 | + cx.cli(["sandbox", "delete", &name]).await?.success()?; |
| 67 | + wait_unlisted(cx, &name).await?; |
| 68 | + cx.expect_absent(&sandbox).await |
| 69 | +} |
| 70 | + |
| 71 | +/// A sandbox created with the CLI can be stopped, rejects `sandbox exec` while |
| 72 | +/// stopped, starts again, and can be stopped and deleted. |
| 73 | +#[testkit::test] |
| 74 | +async fn cli_stop_start_delete(cx: &mut Context) -> testkit::Result { |
| 75 | + let name = cx.name("cl"); |
| 76 | + cx.track(&name); |
| 77 | + cx.cli([ |
| 78 | + "sandbox", |
| 79 | + "create", |
| 80 | + "--name", |
| 81 | + &name, |
| 82 | + "--detach", |
| 83 | + "--no-tty", |
| 84 | + "--", |
| 85 | + "sh", |
| 86 | + "-c", |
| 87 | + "exec sleep infinity", |
| 88 | + ]) |
| 89 | + .await? |
| 90 | + .success()?; |
| 91 | + let sandbox = cx.adopt(&name).await?; |
| 92 | + sandbox.wait_ready().await?; |
| 93 | + |
| 94 | + cx.cli(["sandbox", "stop", &name]).await?.success()?; |
| 95 | + sandbox |
| 96 | + .wait_phase(testkit::sdk::SandboxPhase::Stopped, testkit::STOP_TIMEOUT) |
| 97 | + .await?; |
| 98 | + cx.cli(["sandbox", "exec", "--name", &name, "--no-tty", "--", "true"]) |
| 99 | + .await? |
| 100 | + .failure()?; |
| 101 | + |
| 102 | + cx.cli(["sandbox", "start", &name]).await?.success()?; |
| 103 | + sandbox.wait_ready().await?; |
| 104 | + cx.cli(["sandbox", "stop", &name]).await?.success()?; |
| 105 | + sandbox |
| 106 | + .wait_phase(testkit::sdk::SandboxPhase::Stopped, testkit::STOP_TIMEOUT) |
| 107 | + .await?; |
| 108 | + |
| 109 | + cx.cli(["sandbox", "delete", &name]).await?.success()?; |
| 110 | + wait_unlisted(cx, &name).await?; |
| 111 | + cx.expect_absent(&sandbox).await |
| 112 | +} |
| 113 | + |
| 114 | +/// Wait until `sandbox list` no longer shows the sandbox. |
| 115 | +async fn wait_unlisted(cx: &Context, name: &str) -> testkit::Result { |
| 116 | + testkit::poll_until( |
| 117 | + &format!("sandbox {name} to leave sandbox list"), |
| 118 | + testkit::DELETE_TIMEOUT, |
| 119 | + std::time::Duration::from_secs(1), |
| 120 | + async || { |
| 121 | + Ok(match find_listed(cx, name).await? { |
| 122 | + None => testkit::Poll::Ready(()), |
| 123 | + Some(phase) => testkit::Poll::Pending(format!("still listed in phase {phase}")), |
| 124 | + }) |
| 125 | + }, |
| 126 | + ) |
| 127 | + .await |
| 128 | +} |
| 129 | + |
| 130 | +/// The phase of the sandbox in `sandbox list`, if it is listed. Follows every |
| 131 | +/// page. |
| 132 | +async fn find_listed(cx: &Context, name: &str) -> testkit::Result<Option<String>> { |
| 133 | + let mut token = String::new(); |
| 134 | + for _ in 0..MAX_PAGES { |
| 135 | + let page: Value = cx |
| 136 | + .cli([ |
| 137 | + "sandbox", |
| 138 | + "list", |
| 139 | + "--page-size", |
| 140 | + PAGE_SIZE, |
| 141 | + "--page-token", |
| 142 | + &token, |
| 143 | + "--output", |
| 144 | + "json", |
| 145 | + ]) |
| 146 | + .await? |
| 147 | + .success()? |
| 148 | + .json()?; |
| 149 | + if let Some(entry) = page["sandboxes"] |
| 150 | + .as_array() |
| 151 | + .into_iter() |
| 152 | + .flatten() |
| 153 | + .find(|entry| entry["name"] == name) |
| 154 | + { |
| 155 | + return Ok(Some( |
| 156 | + entry["phase"].as_str().unwrap_or_default().to_string(), |
| 157 | + )); |
| 158 | + } |
| 159 | + token = page["next_page_token"] |
| 160 | + .as_str() |
| 161 | + .unwrap_or_default() |
| 162 | + .to_string(); |
| 163 | + if token.is_empty() { |
| 164 | + return Ok(None); |
| 165 | + } |
| 166 | + } |
| 167 | + Err(testkit::Error::msg(format!( |
| 168 | + "sandbox list has more than {MAX_PAGES} pages" |
| 169 | + ))) |
| 170 | +} |
0 commit comments