feat(workspace): allow creation without an initial repository
Keep bootstrap, signing identity, config and resource-key allocation atomic with optional Repository intent. Preserve repository-present fingerprints for saved receipts; replay reads current authority at the initial key and never recreates missing assets. Provide explicit repository-free CLI creation, optional TUI/Web creation inputs, nullable generated contracts, and valid empty catalog/Worker paths. Retain existing authenticated non-owner viewing policy pending the T-717 authorization decision. Validation: root cargo check; server tests 639 lib + 11 bin; server-api 70 default / 77 typescript; client 52 lib + 2 integration; tui 337; yoi 11 lib + 120 bin. OpenAPI/TS freshness, cargo fmt and diff checks pass. Web check/build, 18 model tests, 268 component tests and Chromium 8 width/theme combinations pass; before/after visual evidence is documented in web/workspace/test/visual-review. Full Web suite has one independently reproduced baseline Ticket source-policy failure.
This commit is contained in:
@@ -4239,22 +4239,27 @@ pub struct InitialRepositoryIntent {
|
||||
pub default_ref: Option<String>,
|
||||
}
|
||||
|
||||
/// Request for atomically creating a Workspace and its initial Repository.
|
||||
/// Request for atomically creating a Workspace with an optional initial Repository.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkspaceCreateRequest {
|
||||
pub operation_key: String,
|
||||
pub display_name: String,
|
||||
pub repository: InitialRepositoryIntent,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub repository: Option<InitialRepositoryIntent>,
|
||||
}
|
||||
|
||||
/// Response returned after atomically creating a Workspace and its first Repository.
|
||||
/// Response returned after atomically creating a Workspace.
|
||||
///
|
||||
/// On replay, Workspace/config and the Repository at the originally requested key are
|
||||
/// current authority, not historical snapshots. Repository is null when creation omitted
|
||||
/// it or that key has since been removed/renamed; replay never recreates an asset.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkspaceCreateResponse {
|
||||
pub workspace: WorkspaceSummary,
|
||||
pub repository: WorkspaceRepositoryRecord,
|
||||
pub repository: Option<WorkspaceRepositoryRecord>,
|
||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||
pub config_revision: u64,
|
||||
@@ -12204,12 +12209,27 @@ mod tests {
|
||||
let request = WorkspaceCreateRequest {
|
||||
operation_key: "workspace-create-1".to_string(),
|
||||
display_name: "Workspace".to_string(),
|
||||
repository: InitialRepositoryIntent {
|
||||
repository: Some(InitialRepositoryIntent {
|
||||
repository_key: "main".to_string(),
|
||||
uri: "/srv/repositories/main".to_string(),
|
||||
default_ref: Some("develop".to_string()),
|
||||
},
|
||||
}),
|
||||
};
|
||||
for repository in [None, Some(serde_json::Value::Null)] {
|
||||
let mut wire =
|
||||
serde_json::json!({"operation_key": "empty", "display_name": "Workspace"});
|
||||
if let Some(repository) = repository {
|
||||
wire["repository"] = repository;
|
||||
}
|
||||
let decoded: WorkspaceCreateRequest = serde_json::from_value(wire).unwrap();
|
||||
assert!(decoded.repository.is_none());
|
||||
assert!(
|
||||
serde_json::to_value(decoded)
|
||||
.unwrap()
|
||||
.get("repository")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
let json = serde_json::to_value(&request).unwrap();
|
||||
assert_eq!(json["operation_key"], "workspace-create-1");
|
||||
assert_eq!(json["repository"]["uri"], "/srv/repositories/main");
|
||||
|
||||
Reference in New Issue
Block a user