sylphx-cli 0.2.24

Sylphx Platform CLI — dogfoods the Rust Management SDK
//! Regression coverage for operator context propagation at the CLI/API boundary.
//!
//! The Management API scopes project routes by organization. A linked project
//! must therefore cause the CLI to send the resolved org selector even when the
//! command does not repeat `--org-id`.

use std::sync::Arc;

use assert_cmd::cargo::cargo_bin;
use serde_json::Value;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::process::Command;
use tokio::sync::{oneshot, Mutex};

const EXPECTED_ORG: &str = "org_test";

struct HeaderServer {
    base_url: String,
    seen_org: Arc<Mutex<Option<String>>>,
    shutdown: Option<oneshot::Sender<()>>,
    join: Option<tokio::task::JoinHandle<()>>,
}

impl HeaderServer {
    async fn start() -> Self {
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();
        let seen_org = Arc::new(Mutex::new(None));
        let seen_org_task = Arc::clone(&seen_org);
        let (shutdown, mut stop) = oneshot::channel();

        let join = tokio::spawn(async move {
            tokio::select! {
                result = listener.accept() => {
                    let (mut socket, _) = result.unwrap();
                    let mut request = Vec::new();
                    loop {
                        let mut chunk = [0u8; 1024];
                        let read = socket.read(&mut chunk).await.unwrap();
                        if read == 0 {
                            break;
                        }
                        request.extend_from_slice(&chunk[..read]);
                        if request.windows(4).any(|window| window == b"\r\n\r\n") {
                            break;
                        }
                    }

                    let request = String::from_utf8_lossy(&request);
                    let org = request.lines().find_map(|line| {
                        let (name, value) = line.split_once(':')?;
                        (name.eq_ignore_ascii_case("x-org-id")).then(|| value.trim().to_string())
                    });
                    *seen_org_task.lock().await = org.clone();

                    let (status, body) = if org.as_deref() == Some(EXPECTED_ORG) {
                        (
                            "200 OK",
                            r#"{"id":"prj_demo","slug":"demo","name":"Demo","orgId":"org_test","isActive":true}"#,
                        )
                    } else {
                        (
                            "404 Not Found",
                            r#"{"error":{"code":"project_not_found","message":"organization context required"}}"#,
                        )
                    };
                    let response = format!(
                        "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
                        body.len()
                    );
                    socket.write_all(response.as_bytes()).await.unwrap();
                }
                _ = &mut stop => {}
            }
        });

        Self {
            base_url: format!("http://{addr}/v1"),
            seen_org,
            shutdown: Some(shutdown),
            join: Some(join),
        }
    }

    async fn shutdown(mut self) {
        if let Some(shutdown) = self.shutdown.take() {
            let _ = shutdown.send(());
        }
        if let Some(join) = self.join.take() {
            let _ = join.await;
        }
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn linked_project_org_is_sent_to_project_scoped_commands() {
    let server = HeaderServer::start().await;
    let workspace = tempfile::tempdir().unwrap();
    let config = tempfile::tempdir().unwrap();
    let link_dir = workspace.path().join(".sylphx");
    std::fs::create_dir_all(&link_dir).unwrap();
    std::fs::write(
        link_dir.join("project.json"),
        r#"{"projectId":"prj_demo","orgId":"org_test"}"#,
    )
    .unwrap();

    let output = Command::new(cargo_bin!("sylphx"))
        .current_dir(workspace.path())
        .env("SYLPHX_CONFIG_DIR", config.path())
        .args([
            "--token",
            "svc_test",
            "--api-url",
            &server.base_url,
            "projects",
            "get",
            "prj_demo",
            "--json",
        ])
        .output()
        .await
        .unwrap();

    assert!(
        output.status.success(),
        "CLI must preserve linked org context; stdout={} stderr={}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    let body: Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(body["id"], "prj_demo");
    assert_eq!(server.seen_org.lock().await.as_deref(), Some(EXPECTED_ORG));

    server.shutdown().await;
}