Files
OpenShell/e2e/rust/tests/workspace_lifecycle.rs
John T. MyersandJohn Myers 07df822090 feat(providers): make profiles authoritative (#2962)
* feat(providers): make profiles authoritative

Closes #1988

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>

* docs(providers): move profiles into provider navigation

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>

* docs(providers): clarify provider attachment lifecycle

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>

* fix(tui): scroll provider profile picker

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>

* fix(providers): honor profile credential semantics

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>

* fix(providers): prefer exact profile IDs

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>

* fix(providers): harden authoritative profile adoption

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>

* test(oidc): align provider fixtures with profiles

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>

* fix(providers): preserve authoritative profile lifecycle

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>

---------

Signed-off-by: John Myers <johntmyers@users.noreply.github.com>
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-09-01 19:22:07 +00:00

279 lines
8.1 KiB
Rust

// 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
);
}