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:
2026-10-08 18:34:44 +09:00
parent e939cd4e4b
commit 7919616db5
37 changed files with 2335 additions and 354 deletions
+55 -32
View File
@@ -199,30 +199,40 @@ fn prompt_create_request_with_io(
if display_name.is_empty() {
return Ok(None);
}
let Some(uri) = prompt_line(input, output, "Initial repository absolute path/URI: ")? else {
return Ok(None);
};
let uri = uri.trim().to_string();
if uri.is_empty() {
writeln!(output, "Repository path/URI is required.")?;
return Ok(None);
}
let repository_key = loop {
let Some(repository_key) = prompt_line(input, output, "Repository key (required): ")?
else {
return Ok(None);
};
if repository_key.trim().is_empty() {
writeln!(output, "Repository key is required.")?;
continue;
}
break repository_key;
};
let Some(default_ref) = prompt_line(input, output, "Default ref [repository default]: ")?
let Some(uri) = prompt_line(
input,
output,
"Initial repository absolute path/URI (optional, Enter to skip): ",
)?
else {
return Ok(None);
};
let default_ref = default_ref.trim().to_string();
let uri = uri.trim().to_string();
let repository = if uri.is_empty() {
None
} else {
let repository_key = loop {
let Some(repository_key) = prompt_line(input, output, "Repository key (required): ")?
else {
return Ok(None);
};
if repository_key.trim().is_empty() {
writeln!(output, "Repository key is required.")?;
continue;
}
break repository_key;
};
let Some(default_ref) = prompt_line(input, output, "Default ref [repository default]: ")?
else {
return Ok(None);
};
let default_ref = default_ref.trim().to_string();
Some(CreateBackendWorkspaceRepository {
uri,
repository_key,
default_ref: (!default_ref.is_empty()).then_some(default_ref),
})
};
let operation_key = format!(
"tui-workspace-create-{}-{}",
std::process::id(),
@@ -234,11 +244,7 @@ fn prompt_create_request_with_io(
Ok(Some(CreateBackendWorkspaceRequest {
operation_key,
display_name,
repository: CreateBackendWorkspaceRepository {
uri,
repository_key,
default_ref: (!default_ref.is_empty()).then_some(default_ref),
},
repository,
}))
}
@@ -286,15 +292,29 @@ mod tests {
);
}
#[test]
fn workspace_creation_without_initial_repository_skips_repository_prompts() {
let (request, output) = prompt_create("Workspace\n\n");
assert!(request.unwrap().repository.is_none());
assert!(!output.contains("Repository key (required)"));
assert!(!output.contains("Default ref"));
}
#[test]
fn workspace_creation_preserves_an_explicit_non_main_repository_key() {
let (request, output) = prompt_create("Platform\n/srv/platform\nplatform\ndevelop\n");
let request = request.expect("complete input should create a request");
assert_eq!(request.display_name, "Platform");
assert_eq!(request.repository.uri, "/srv/platform");
assert_eq!(request.repository.repository_key, "platform");
assert_eq!(request.repository.default_ref.as_deref(), Some("develop"));
assert_eq!(request.repository.as_ref().unwrap().uri, "/srv/platform");
assert_eq!(
request.repository.as_ref().unwrap().repository_key,
"platform"
);
assert_eq!(
request.repository.as_ref().unwrap().default_ref.as_deref(),
Some("develop")
);
assert!(output.contains("Repository key (required): "));
assert!(!output.contains("Repository key [main]"));
}
@@ -304,7 +324,10 @@ mod tests {
let (request, output) = prompt_create("Platform\n/srv/platform\n\n \nplatform\n\n");
let request = request.expect("a later explicit key should create a request");
assert_eq!(request.repository.repository_key, "platform");
assert_eq!(
request.repository.as_ref().unwrap().repository_key,
"platform"
);
assert_eq!(output.matches("Repository key is required.").count(), 2);
assert_eq!(output.matches("Repository key (required): ").count(), 3);
}
@@ -314,7 +337,7 @@ mod tests {
let (request, _) = prompt_create("Workspace\n/srv/repository\n platform \n\n");
assert_eq!(
request.unwrap().repository.repository_key,
request.unwrap().repository.unwrap().repository_key,
" platform ",
"the Backend must validate the creator's exact Repository key"
);
@@ -325,7 +348,7 @@ mod tests {
let (request, _) = prompt_create("Workspace\n/srv/repository\nmain\n\n");
assert_eq!(
request.unwrap().repository.repository_key,
request.unwrap().repository.unwrap().repository_key,
"main",
"main remains valid when the creator enters it explicitly"
);