fix: share workspace creation wire contract

This commit is contained in:
2026-09-14 19:45:18 +09:00
parent 3995561220
commit c08152d069
6 changed files with 70 additions and 53 deletions
+9 -23
View File
@@ -1,32 +1,18 @@
use crate::{BackendApiClient, BackendApiClientError}; use crate::{BackendApiClient, BackendApiClientError};
use reqwest::Method; use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use workspace_api::{ use workspace_api::{
RepositoryListResponse, RepositorySummary, WorkspaceCatalogListResponse, InitialRepositoryIntent, RepositoryListResponse, RepositorySummary,
WorkspaceCreateResponse, WorkspaceSummary, WorkspaceCatalogListResponse, WorkspaceCreateRequest, WorkspaceCreateResponse,
WorkspaceSummary,
}; };
const DEFAULT_WORKSPACE_LIMIT: usize = 200; const DEFAULT_WORKSPACE_LIMIT: usize = 200;
pub type BackendWorkspace = WorkspaceSummary; pub type BackendWorkspace = WorkspaceSummary;
pub type CreateBackendWorkspaceResponse = WorkspaceCreateResponse; pub type CreateBackendWorkspaceResponse = WorkspaceCreateResponse;
pub type CreateBackendWorkspaceRequest = WorkspaceCreateRequest;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub type CreateBackendWorkspaceRepository = InitialRepositoryIntent;
#[serde(deny_unknown_fields)]
pub struct CreateBackendWorkspaceRequest {
pub operation_id: String,
pub display_name: String,
pub repository: CreateBackendWorkspaceRepository,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct CreateBackendWorkspaceRepository {
pub repository_key: String,
pub source: String,
pub default_ref: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendWorkspaceCatalogTarget { pub struct BackendWorkspaceCatalogTarget {
@@ -185,19 +171,19 @@ mod tests {
} }
#[test] #[test]
fn create_request_keeps_operation_id_for_exact_retry() { fn create_request_keeps_operation_key_for_exact_retry() {
let request = CreateBackendWorkspaceRequest { let request = CreateBackendWorkspaceRequest {
operation_id: "workspace-create-1".to_string(), operation_key: "workspace-create-1".to_string(),
display_name: "Alpha".to_string(), display_name: "Alpha".to_string(),
repository: CreateBackendWorkspaceRepository { repository: CreateBackendWorkspaceRepository {
repository_key: "main".to_string(), repository_key: "main".to_string(),
source: "/srv/repos/alpha".to_string(), uri: "/srv/repos/alpha".to_string(),
default_ref: Some("develop".to_string()), default_ref: Some("develop".to_string()),
}, },
}; };
let retry = request.clone(); let retry = request.clone();
assert_eq!(retry.operation_id, "workspace-create-1"); assert_eq!(retry.operation_key, "workspace-create-1");
assert_eq!(retry, request); assert_eq!(retry, request);
} }
} }
+8 -8
View File
@@ -4,26 +4,26 @@ use client::{
}; };
#[test] #[test]
fn workspace_creation_request_preserves_operation_id_for_retry() { fn workspace_creation_request_preserves_operation_key_for_retry() {
let request = CreateBackendWorkspaceRequest { let request = CreateBackendWorkspaceRequest {
operation_id: "workspace-create-1".to_string(), operation_key: "workspace-create-1".to_string(),
display_name: "Alpha".to_string(), display_name: "Alpha".to_string(),
repository: CreateBackendWorkspaceRepository { repository: CreateBackendWorkspaceRepository {
repository_key: "main".to_string(), repository_key: "main".to_string(),
source: "/srv/repos/alpha".to_string(), uri: "/srv/repos/alpha".to_string(),
default_ref: Some("develop".to_string()), default_ref: Some("develop".to_string()),
}, },
}; };
assert_eq!(request.clone(), request); assert_eq!(request.clone(), request);
assert_eq!(request.operation_id, "workspace-create-1"); assert_eq!(request.operation_key, "workspace-create-1");
let json = serde_json::to_value(&request).unwrap(); let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["operation_id"], "workspace-create-1"); assert_eq!(json["operation_key"], "workspace-create-1");
assert_eq!(json["repository"]["repository_key"], "main"); assert_eq!(json["repository"]["repository_key"], "main");
assert_eq!(json["repository"]["source"], "/srv/repos/alpha"); assert_eq!(json["repository"]["uri"], "/srv/repos/alpha");
assert!(json.get("operation_key").is_none()); assert!(json.get("operation_id").is_none());
assert!(json["repository"].get("display_name").is_none()); assert!(json["repository"].get("display_name").is_none());
assert!(json["repository"].get("uri").is_none()); assert!(json["repository"].get("source").is_none());
} }
#[test] #[test]
+2 -2
View File
@@ -200,10 +200,10 @@ fn prompt_create_request_inner() -> PickerResult<Option<CreateBackendWorkspaceRe
.as_nanos() .as_nanos()
); );
Ok(Some(CreateBackendWorkspaceRequest { Ok(Some(CreateBackendWorkspaceRequest {
operation_id: operation_key, operation_key,
display_name, display_name,
repository: CreateBackendWorkspaceRepository { repository: CreateBackendWorkspaceRepository {
source: uri, uri,
repository_key: if repository_key.is_empty() { repository_key: if repository_key.is_empty() {
"main".to_string() "main".to_string()
} else { } else {
+48
View File
@@ -520,6 +520,25 @@ pub struct WorkspaceRepositoryRecord {
pub updated_at: String, pub updated_at: String,
} }
/// Initial Repository registration intent for Workspace creation.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct InitialRepositoryIntent {
pub repository_key: String,
pub uri: String,
#[serde(default)]
pub default_ref: Option<String>,
}
/// Request for atomically creating a Workspace and its initial Repository.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceCreateRequest {
pub operation_key: String,
pub display_name: String,
pub repository: InitialRepositoryIntent,
}
/// Response returned after atomically creating a Workspace and its first Repository. /// Response returned after atomically creating a Workspace and its first Repository.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
@@ -3899,6 +3918,35 @@ mod tests {
); );
} }
#[test]
fn workspace_create_request_has_one_closed_shared_wire_shape() {
let request = WorkspaceCreateRequest {
operation_key: "workspace-create-1".to_string(),
display_name: "Workspace".to_string(),
repository: InitialRepositoryIntent {
repository_key: "main".to_string(),
uri: "/srv/repositories/main".to_string(),
default_ref: Some("develop".to_string()),
},
};
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["operation_key"], "workspace-create-1");
assert_eq!(json["repository"]["uri"], "/srv/repositories/main");
assert!(json.get("operation_id").is_none());
assert!(json["repository"].get("source").is_none());
assert!(
serde_json::from_value::<WorkspaceCreateRequest>(serde_json::json!({
"operation_id": "workspace-create-1",
"display_name": "Workspace",
"repository": {
"repository_key": "main",
"source": "/srv/repositories/main"
}
}))
.is_err()
);
}
#[test] #[test]
fn workspace_and_repository_response_shapes_round_trip() { fn workspace_and_repository_response_shapes_round_trip() {
let workspace = serde_json::json!({ let workspace = serde_json::json!({
@@ -1,10 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use chrono::{SecondsFormat, Utc}; use chrono::{SecondsFormat, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use uuid::Uuid; use uuid::Uuid;
pub use workspace_api::{InitialRepositoryIntent, WorkspaceCreateRequest};
use workspace_api::{RepositoryObservedStatus, RepositorySource}; use workspace_api::{RepositoryObservedStatus, RepositorySource};
use crate::repository_source::{parse_repository_source, repository_source_fingerprint}; use crate::repository_source::{parse_repository_source, repository_source_fingerprint};
@@ -19,23 +19,6 @@ use crate::{Error, Result};
const MAX_DISPLAY_NAME_BYTES: usize = 200; const MAX_DISPLAY_NAME_BYTES: usize = 200;
const MAX_OPERATION_KEY_BYTES: usize = 200; const MAX_OPERATION_KEY_BYTES: usize = 200;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct InitialRepositoryIntent {
pub repository_key: String,
pub uri: String,
#[serde(default)]
pub default_ref: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceCreateRequest {
pub operation_key: String,
pub display_name: String,
pub repository: InitialRepositoryIntent,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceCreateResult { pub struct WorkspaceCreateResult {
pub workspace: WorkspaceRecord, pub workspace: WorkspaceRecord,
+2 -2
View File
@@ -32,11 +32,11 @@ pub(crate) async fn run_init(
let response = create_backend_workspace( let response = create_backend_workspace(
&target, &target,
&CreateBackendWorkspaceRequest { &CreateBackendWorkspaceRequest {
operation_id: uuid::Uuid::now_v7().to_string(), operation_key: uuid::Uuid::now_v7().to_string(),
display_name: options.display_name, display_name: options.display_name,
repository: CreateBackendWorkspaceRepository { repository: CreateBackendWorkspaceRepository {
repository_key: options.repository_key, repository_key: options.repository_key,
source: repository_uri.to_string(), uri: repository_uri.to_string(),
default_ref: options.default_ref, default_ref: options.default_ref,
}, },
}, },