diff --git a/apps/cli/src/caps.rs b/apps/cli/src/caps.rs index 088a14f7122..a5382c5d92c 100644 --- a/apps/cli/src/caps.rs +++ b/apps/cli/src/caps.rs @@ -159,6 +159,17 @@ struct LoomImportArgs { format: OutputFormat, } +fn loom_import_payload(loom_url: &str, owner_email: Option<&str>, space: Option<&str>) -> Value { + let mut payload = json!({ "loomUrl": loom_url }); + if let Some(owner_email) = owner_email { + payload["ownerEmail"] = json!(owner_email); + } + if let Some(space) = space { + payload["spaceName"] = json!(space); + } + payload +} + #[derive(Args)] struct WaitArgs { cap: String, @@ -1274,11 +1285,11 @@ impl CapsArgs { .mutate_json_confirmed( Method::POST, &format!("/organizations/{organization}/imports/loom"), - &json!({ - "loomUrl": args.loom_url, - "ownerEmail": args.owner_email, - "spaceName": args.space, - }), + &loom_import_payload( + &args.loom_url, + args.owner_email.as_deref(), + args.space.as_deref(), + ), ) .await?; let value = if args.wait { @@ -1763,6 +1774,26 @@ mod tests { use super::*; use tokio::io::{AsyncReadExt, AsyncWriteExt}; + #[test] + fn loom_import_payload_omits_unset_optional_fields() { + assert_eq!( + loom_import_payload("https://www.loom.com/share/abc", None, None), + json!({ "loomUrl": "https://www.loom.com/share/abc" }) + ); + assert_eq!( + loom_import_payload( + "https://www.loom.com/share/abc", + Some("owner@example.com"), + Some("Team"), + ), + json!({ + "loomUrl": "https://www.loom.com/share/abc", + "ownerEmail": "owner@example.com", + "spaceName": "Team", + }) + ); + } + async fn read_request(stream: &mut tokio::net::TcpStream) -> String { let mut bytes = Vec::new(); let mut buffer = [0_u8; 1_024]; diff --git a/apps/web/__tests__/unit/agent-api-contract.test.ts b/apps/web/__tests__/unit/agent-api-contract.test.ts index 18078ced76b..c288cdb9d39 100644 --- a/apps/web/__tests__/unit/agent-api-contract.test.ts +++ b/apps/web/__tests__/unit/agent-api-contract.test.ts @@ -16,6 +16,26 @@ const status = { }; describe("agent API contract", () => { + it("accepts omitted and null Loom import options from installed clients", () => { + const loomUrl = "https://www.loom.com/share/synthetic"; + const decode = Schema.decodeUnknownSync(Agent.AgentLoomImportInput); + + expect(decode({ loomUrl })).toEqual({ loomUrl }); + expect(decode({ loomUrl, ownerEmail: null, spaceName: null })).toEqual({ + loomUrl, + ownerEmail: null, + spaceName: null, + }); + expect( + decode({ + loomUrl, + ownerEmail: "owner@example.com", + spaceName: "Team", + }), + ).toEqual({ loomUrl, ownerEmail: "owner@example.com", spaceName: "Team" }); + expect(() => decode({ loomUrl, ownerEmail: 42 })).toThrow(); + }); + it("verifies credentials online and rate limits authorization boundaries", () => { const contract = readFileSync( join(process.cwd(), "../../packages/web-domain/src/Agent.ts"), diff --git a/packages/web-domain/src/Agent.ts b/packages/web-domain/src/Agent.ts index 991f058a839..2ac134832e2 100644 --- a/packages/web-domain/src/Agent.ts +++ b/packages/web-domain/src/Agent.ts @@ -1132,8 +1132,8 @@ export const AgentUploadCompleteResponse = Schema.Struct({ export const AgentLoomImportInput = Schema.Struct({ loomUrl: Schema.String, - ownerEmail: Schema.optional(Schema.String), - spaceName: Schema.optional(Schema.String), + ownerEmail: Schema.optional(Schema.NullOr(Schema.String)), + spaceName: Schema.optional(Schema.NullOr(Schema.String)), }); export const AgentProfileUpdateInput = Schema.Struct({