diff --git a/cli/tests/prompt_structured_test.rs b/cli/tests/prompt_structured_test.rs index 7576003f..9cce2329 100644 --- a/cli/tests/prompt_structured_test.rs +++ b/cli/tests/prompt_structured_test.rs @@ -861,6 +861,91 @@ fn test_cli_push_prompt_update_with_auto_parent() { // CLEANUP: Automatic via Drop trait } +/// Deletes a repo by name on drop without creating it first, for tests where the CLI creates the repo. +struct NewRepoCleanup { + repo_name: String, + runtime: tokio::runtime::Runtime, + client: LangchainClient, +} + +impl NewRepoCleanup { + fn new(repo_name: &str) -> Self { + Self { + repo_name: repo_name.to_string(), + runtime: create_runtime(), + client: create_sdk_client().expect("Failed to create SDK client"), + } + } +} + +impl Drop for NewRepoCleanup { + fn drop(&mut self) { + println!("[CLEANUP] Deleting repo: {}", self.repo_name); + let _ = self + .runtime + .block_on(async { self.client.prompts().delete(&self.repo_name).await }); + } +} + +/// Guards [Issue #820 prompt push --owner - fails when it creates a new repo](https://github.com/codekiln/langstar/issues/820): pushing with `--owner -` to a repo that does not exist creates the repo and its first commit. +#[test] +#[cfg_attr(not(feature = "integration-tests"), ignore)] +#[serial] +fn test_cli_push_creates_new_repo_with_owner_dash() { + check_env_vars_private_prompts(); + + let repo_name = generate_unique_repo_name("test-push-owner-dash"); + // CLEANUP: declared before the push so the repo is deleted even if an assertion fails + let _cleanup = NewRepoCleanup::new(&repo_name); + + let schema_file = create_temp_schema_file(); + let schema_path = schema_file.path().to_str().unwrap(); + let bin = get_langstar_bin(); + + // TEST: push to a repo that does not exist yet + let mut push_cmd = Command::new(&bin); + push_cmd.env("LANGSTAR_CONFIG_DIR", config_dir::empty_config_dir()); + push_cmd.args([ + "prompt", + "push", + "--owner", + "-", + "--repo", + &repo_name, + "--template", + "Owner dash push: {input}", + "--input-variables", + "input", + "--schema", + schema_path, + ]); + push_cmd.assert().success(); + + // VERIFY: the pulled manifest has the pushed template and schema + let mut pull_cmd = Command::new(&bin); + pull_cmd.env("LANGSTAR_CONFIG_DIR", config_dir::empty_config_dir()); + pull_cmd.args([ + "prompt", + "pull", + "--format", + "json", + "--", + &format!("-/{}", repo_name), + ]); + let output = pull_cmd.assert().success(); + let stdout = String::from_utf8_lossy(&output.get_output().stdout); + assert!( + stdout.contains("Owner dash push: {input}"), + "pulled manifest should contain the pushed template, got: {}", + stdout + ); + assert!( + stdout.contains("TestOutput"), + "pulled manifest should contain the pushed schema title, got: {}", + stdout + ); +} + /// Pushes a plain prompt without `--schema` to an empty repo, pulls it back, and checks that the API stored a `ChatPromptTemplate` with an LC-JSON `id`. /// The API used to answer this push with 400 "Manifest must have an id field", reported in [prompt push without --schema to an empty repo fails with "Manifest must have an id field" ยท Issue #821](https://github.com/codekiln/langstar/issues/821). #[test] diff --git a/sdk/src/prompts.rs b/sdk/src/prompts.rs index a983f8e7..90f700f5 100644 --- a/sdk/src/prompts.rs +++ b/sdk/src/prompts.rs @@ -356,6 +356,12 @@ pub struct Prompt { pub is_public: bool, } +/// The `repo_handle` to send when creating a repo: `-/name` (`-` is the current +/// workspace) becomes `name`, and every other handle is sent as given. +fn strip_dash_owner(repo_handle: &str) -> &str { + repo_handle.strip_prefix("-/").unwrap_or(repo_handle) +} + /// Client for interacting with LangSmith Prompts API pub struct PromptClient<'a> { client: &'a LangchainClient, @@ -521,7 +527,12 @@ impl<'a> PromptClient<'a> { /// Create a new prompt repository /// /// # Arguments - /// * `repo_handle` - The handle for the repository (e.g., "owner/repo-name") + /// * `repo_handle` - The handle for the repository (e.g., "owner/repo-name"). A + /// `-/repo-name` handle, where `-` means the current workspace, is sent as + /// just `repo-name`, because the API accepts only lowercase letters, digits, + /// hyphens and underscores in a new repo's handle. + /// [`langsmith-sdk`](https://github.com/langchain-ai/langsmith-sdk/blob/9966fd69a631369398d7598d8d725868f9c74339/python/langsmith/client.py#L9904) + /// also sends only the name. /// * `description` - Optional description /// * `readme` - Optional readme content /// * `is_public` - Whether the repository is public (default: false) @@ -546,7 +557,7 @@ impl<'a> PromptClient<'a> { } let request_body = CreateRepoRequest { - repo_handle: repo_handle.to_string(), + repo_handle: strip_dash_owner(repo_handle).to_string(), description, readme, is_public, @@ -1319,6 +1330,17 @@ mod tests { use crate::auth::AuthConfig; use serde_json::json; + #[test] + fn test_strip_dash_owner_drops_dash() { + assert_eq!(strip_dash_owner("-/my-prompt"), "my-prompt"); + } + + #[test] + fn test_strip_dash_owner_keeps_named_owner_and_bare_name() { + assert_eq!(strip_dash_owner("acme/my-prompt"), "acme/my-prompt"); + assert_eq!(strip_dash_owner("my-prompt"), "my-prompt"); + } + #[test] fn test_prompt_client_creation() { let auth = AuthConfig::new(Some("test".to_string()), None, None);