tui: configure orchestration branch
This commit is contained in:
parent
c4465a04d8
commit
1c54689edb
|
|
@ -35,6 +35,9 @@ pub fn ticket_config_scaffold() -> String {
|
||||||
out.push_str(
|
out.push_str(
|
||||||
"\n# Optional durable Ticket record language. When unset, generated Ticket text keeps current defaults.\n# [ticket]\n# language = \"Japanese\"\n",
|
"\n# Optional durable Ticket record language. When unset, generated Ticket text keeps current defaults.\n# [ticket]\n# language = \"Japanese\"\n",
|
||||||
);
|
);
|
||||||
|
out.push_str(
|
||||||
|
"\n# Optional Panel Orchestrator worktree branch. When unset, Panel uses orchestration/<workspace-orchestrator-pod-name>.\n# [orchestration]\n# branch = \"orchestration/<workspace-orchestrator-pod-name>\"\n",
|
||||||
|
);
|
||||||
for role in TicketRole::ALL {
|
for role in TicketRole::ALL {
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"\n[roles.{role}]\nprofile = \"{}\"\nworkflow = \"{}\"\n",
|
"\n[roles.{role}]\nprofile = \"{}\"\nworkflow = \"{}\"\n",
|
||||||
|
|
@ -67,15 +70,110 @@ pub enum TicketConfigError {
|
||||||
pub struct TicketConfig {
|
pub struct TicketConfig {
|
||||||
pub backend: TicketBackendConfig,
|
pub backend: TicketBackendConfig,
|
||||||
pub ticket: TicketRecordConfig,
|
pub ticket: TicketRecordConfig,
|
||||||
|
pub orchestration: TicketOrchestrationConfig,
|
||||||
pub roles: TicketRoleProfiles,
|
pub roles: TicketRoleProfiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct TicketOrchestrationConfig {
|
||||||
|
pub branch: Option<GitBranchName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TicketOrchestrationConfig {
|
||||||
|
pub fn branch_name(&self) -> Option<&str> {
|
||||||
|
self.branch.as_ref().map(GitBranchName::as_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||||
|
pub struct GitBranchName(String);
|
||||||
|
|
||||||
|
impl GitBranchName {
|
||||||
|
pub fn new(value: impl Into<String>) -> Result<Self, String> {
|
||||||
|
let value = value.into();
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed != value {
|
||||||
|
return Err("git branch name must not have leading or trailing whitespace".to_string());
|
||||||
|
}
|
||||||
|
validate_git_branch_name_value(trimmed)?;
|
||||||
|
Ok(Self(trimmed.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for GitBranchName {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = String::deserialize(deserializer)?;
|
||||||
|
Self::new(value).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for GitBranchName {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_git_branch_name_value(value: &str) -> Result<(), String> {
|
||||||
|
if value.is_empty() {
|
||||||
|
return Err("git branch name must not be empty".to_string());
|
||||||
|
}
|
||||||
|
if value == "@" {
|
||||||
|
return Err("git branch name must not be `@`".to_string());
|
||||||
|
}
|
||||||
|
if value.starts_with('-') {
|
||||||
|
return Err("git branch name must not start with `-`".to_string());
|
||||||
|
}
|
||||||
|
if value.starts_with("refs/") {
|
||||||
|
return Err("git branch name must be a short branch name, not a full ref".to_string());
|
||||||
|
}
|
||||||
|
if value.starts_with('/') || value.ends_with('/') || value.contains("//") {
|
||||||
|
return Err("git branch name must not contain empty path components".to_string());
|
||||||
|
}
|
||||||
|
if value.contains("..") {
|
||||||
|
return Err("git branch name must not contain `..`".to_string());
|
||||||
|
}
|
||||||
|
if value.contains("@{") {
|
||||||
|
return Err("git branch name must not contain `@{`".to_string());
|
||||||
|
}
|
||||||
|
if value.ends_with('.') {
|
||||||
|
return Err("git branch name must not end with `.`".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in value.split('/') {
|
||||||
|
if component.starts_with('.') {
|
||||||
|
return Err("git branch name components must not start with `.`".to_string());
|
||||||
|
}
|
||||||
|
if component.ends_with(".lock") {
|
||||||
|
return Err("git branch name components must not end with `.lock`".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ch in value.chars() {
|
||||||
|
if ch.is_control() || matches!(ch, ' ' | '~' | '^' | ':' | '?' | '*' | '[' | '\\') {
|
||||||
|
return Err(format!(
|
||||||
|
"git branch name contains unsupported character `{}`",
|
||||||
|
ch.escape_default()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
impl TicketConfig {
|
impl TicketConfig {
|
||||||
pub fn default_for_workspace(workspace_root: impl AsRef<Path>) -> Self {
|
pub fn default_for_workspace(workspace_root: impl AsRef<Path>) -> Self {
|
||||||
let workspace_root = workspace_root.as_ref();
|
let workspace_root = workspace_root.as_ref();
|
||||||
Self {
|
Self {
|
||||||
backend: TicketBackendConfig::default_for_workspace(workspace_root),
|
backend: TicketBackendConfig::default_for_workspace(workspace_root),
|
||||||
ticket: TicketRecordConfig::default(),
|
ticket: TicketRecordConfig::default(),
|
||||||
|
orchestration: TicketOrchestrationConfig::default(),
|
||||||
roles: TicketRoleProfiles::default(),
|
roles: TicketRoleProfiles::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -528,9 +626,26 @@ struct RawTicketConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
ticket: RawTicketRecordConfig,
|
ticket: RawTicketRecordConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
orchestration: RawTicketOrchestrationConfig,
|
||||||
|
#[serde(default)]
|
||||||
roles: BTreeMap<String, RawTicketRoleConfig>,
|
roles: BTreeMap<String, RawTicketRoleConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct RawTicketOrchestrationConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
branch: Option<GitBranchName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawTicketOrchestrationConfig {
|
||||||
|
fn resolve(self) -> TicketOrchestrationConfig {
|
||||||
|
TicketOrchestrationConfig {
|
||||||
|
branch: self.branch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct RawTicketRecordConfig {
|
struct RawTicketRecordConfig {
|
||||||
|
|
@ -576,6 +691,7 @@ impl RawTicketConfig {
|
||||||
}
|
}
|
||||||
})?,
|
})?,
|
||||||
ticket: self.ticket.resolve(),
|
ticket: self.ticket.resolve(),
|
||||||
|
orchestration: self.orchestration.resolve(),
|
||||||
roles,
|
roles,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -680,6 +796,7 @@ mod tests {
|
||||||
temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)
|
temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)
|
||||||
);
|
);
|
||||||
assert_eq!(config.ticket_record_language(), None);
|
assert_eq!(config.ticket_record_language(), None);
|
||||||
|
assert_eq!(config.orchestration.branch_name(), None);
|
||||||
for role in TicketRole::ALL {
|
for role in TicketRole::ALL {
|
||||||
let role_config = config.role(role);
|
let role_config = config.role(role);
|
||||||
assert_eq!(role_config.profile.as_str(), "inherit");
|
assert_eq!(role_config.profile.as_str(), "inherit");
|
||||||
|
|
@ -701,6 +818,9 @@ root = "custom-tickets"
|
||||||
[ticket]
|
[ticket]
|
||||||
language = "Japanese"
|
language = "Japanese"
|
||||||
|
|
||||||
|
[orchestration]
|
||||||
|
branch = "orchestration/custom-panel"
|
||||||
|
|
||||||
[roles.intake]
|
[roles.intake]
|
||||||
profile = "project:intake"
|
profile = "project:intake"
|
||||||
launch_prompt = "$workspace/ticket/intake/launch"
|
launch_prompt = "$workspace/ticket/intake/launch"
|
||||||
|
|
@ -730,6 +850,10 @@ workflow = "multi-agent-workflow"
|
||||||
);
|
);
|
||||||
assert_eq!(config.backend.root, temp.path().join("custom-tickets"));
|
assert_eq!(config.backend.root, temp.path().join("custom-tickets"));
|
||||||
assert_eq!(config.ticket_record_language(), Some("Japanese"));
|
assert_eq!(config.ticket_record_language(), Some("Japanese"));
|
||||||
|
assert_eq!(
|
||||||
|
config.orchestration.branch_name(),
|
||||||
|
Some("orchestration/custom-panel")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config.profile_for(TicketRole::Intake).as_str(),
|
config.profile_for(TicketRole::Intake).as_str(),
|
||||||
"project:intake"
|
"project:intake"
|
||||||
|
|
@ -756,6 +880,9 @@ workflow = "multi-agent-workflow"
|
||||||
assert!(scaffold.contains("provider = \"builtin:yoi_local\""));
|
assert!(scaffold.contains("provider = \"builtin:yoi_local\""));
|
||||||
assert!(scaffold.contains("root = \".yoi/tickets\""));
|
assert!(scaffold.contains("root = \".yoi/tickets\""));
|
||||||
assert!(scaffold.contains("# [ticket]\n# language = \"Japanese\""));
|
assert!(scaffold.contains("# [ticket]\n# language = \"Japanese\""));
|
||||||
|
assert!(scaffold.contains(
|
||||||
|
"# [orchestration]\n# branch = \"orchestration/<workspace-orchestrator-pod-name>\""
|
||||||
|
));
|
||||||
for role in TicketRole::ALL {
|
for role in TicketRole::ALL {
|
||||||
assert!(scaffold.contains(&format!("[roles.{role}]")));
|
assert!(scaffold.contains(&format!("[roles.{role}]")));
|
||||||
assert!(scaffold.contains(&format!(
|
assert!(scaffold.contains(&format!(
|
||||||
|
|
@ -773,6 +900,7 @@ workflow = "multi-agent-workflow"
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(config.backend_root(), temp.path().join(".yoi/tickets"));
|
assert_eq!(config.backend_root(), temp.path().join(".yoi/tickets"));
|
||||||
|
assert_eq!(config.orchestration.branch_name(), None);
|
||||||
for role in TicketRole::ALL {
|
for role in TicketRole::ALL {
|
||||||
let role_config = config.role_launch_config(role).unwrap();
|
let role_config = config.role_launch_config(role).unwrap();
|
||||||
assert_eq!(role_config.profile.as_str(), role.default_profile());
|
assert_eq!(role_config.profile.as_str(), role.default_profile());
|
||||||
|
|
@ -851,6 +979,32 @@ profile = "builtin:default"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn orchestration_branch_config_is_validated_as_git_branch_name() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
write_config(
|
||||||
|
temp.path(),
|
||||||
|
r#"
|
||||||
|
[orchestration]
|
||||||
|
branch = "orchestration/panel:bad"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let error = TicketConfig::load_workspace(temp.path()).unwrap_err();
|
||||||
|
assert!(error.to_string().contains("git branch name"));
|
||||||
|
assert!(error.to_string().contains("unsupported character"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn orchestration_branch_rejects_full_refs_and_dash_prefixes() {
|
||||||
|
assert!(GitBranchName::new("refs/heads/orchestration/panel").is_err());
|
||||||
|
assert!(GitBranchName::new("-orchestration-panel").is_err());
|
||||||
|
assert_eq!(
|
||||||
|
GitBranchName::new("orchestration/panel").unwrap().as_str(),
|
||||||
|
"orchestration/panel"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn role_table_without_profile_is_not_role_launch_ready() {
|
fn role_table_without_profile_is_not_role_launch_ready() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap};
|
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use session_store::FsStore;
|
use session_store::FsStore;
|
||||||
use ticket::config::TicketConfig;
|
use ticket::config::{GitBranchName, TicketConfig};
|
||||||
use ticket::{LocalTicketBackend, TicketBackend, TicketIdOrSlug, TicketWorkflowState};
|
use ticket::{LocalTicketBackend, TicketBackend, TicketIdOrSlug, TicketWorkflowState};
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
@ -2170,17 +2170,43 @@ struct OrchestrationWorktreeReady {
|
||||||
status: OrchestrationWorktreeStatus,
|
status: OrchestrationWorktreeStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn orchestration_worktree_layout(workspace_root: &Path) -> OrchestrationWorktreeLayout {
|
fn orchestration_worktree_layout_for_branch(
|
||||||
|
workspace_root: &Path,
|
||||||
|
branch: String,
|
||||||
|
) -> OrchestrationWorktreeLayout {
|
||||||
let stem = workspace_orchestrator_pod_name(workspace_root);
|
let stem = workspace_orchestrator_pod_name(workspace_root);
|
||||||
OrchestrationWorktreeLayout {
|
OrchestrationWorktreeLayout {
|
||||||
path: workspace_root
|
path: workspace_root
|
||||||
.join(".worktree")
|
.join(".worktree")
|
||||||
.join("orchestration")
|
.join("orchestration")
|
||||||
.join(&stem),
|
.join(&stem),
|
||||||
branch: format!("orchestration/{stem}"),
|
branch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn orchestration_worktree_layout(workspace_root: &Path) -> OrchestrationWorktreeLayout {
|
||||||
|
let stem = workspace_orchestrator_pod_name(workspace_root);
|
||||||
|
orchestration_worktree_layout_for_branch(workspace_root, format!("orchestration/{stem}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolved_orchestration_worktree_layout(
|
||||||
|
workspace_root: &Path,
|
||||||
|
) -> Result<OrchestrationWorktreeLayout, String> {
|
||||||
|
let config = TicketConfig::load_workspace(workspace_root)
|
||||||
|
.map_err(|err| format!("failed to load ticket config for orchestration branch: {err}"))?;
|
||||||
|
let branch = if let Some(branch) = config.orchestration.branch_name() {
|
||||||
|
branch.to_string()
|
||||||
|
} else {
|
||||||
|
orchestration_worktree_layout(workspace_root).branch
|
||||||
|
};
|
||||||
|
GitBranchName::new(branch.clone())
|
||||||
|
.map_err(|message| format!("invalid orchestration branch `{branch}`: {message}"))?;
|
||||||
|
Ok(orchestration_worktree_layout_for_branch(
|
||||||
|
workspace_root,
|
||||||
|
branch,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn build_orchestrator_launch_context(
|
fn build_orchestrator_launch_context(
|
||||||
original_workspace_root: &Path,
|
original_workspace_root: &Path,
|
||||||
orchestration_workspace_root: &Path,
|
orchestration_workspace_root: &Path,
|
||||||
|
|
@ -2204,7 +2230,7 @@ fn build_orchestrator_launch_context(
|
||||||
fn ensure_orchestration_worktree(
|
fn ensure_orchestration_worktree(
|
||||||
workspace_root: &Path,
|
workspace_root: &Path,
|
||||||
) -> Result<OrchestrationWorktreeReady, String> {
|
) -> Result<OrchestrationWorktreeReady, String> {
|
||||||
let layout = orchestration_worktree_layout(workspace_root);
|
let layout = resolved_orchestration_worktree_layout(workspace_root)?;
|
||||||
if layout.path.exists() {
|
if layout.path.exists() {
|
||||||
if !layout.path.is_dir() {
|
if !layout.path.is_dir() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|
@ -2267,7 +2293,7 @@ fn ensure_orchestration_worktree(
|
||||||
fn prepare_orchestration_worktree_for_restore(
|
fn prepare_orchestration_worktree_for_restore(
|
||||||
workspace_root: &Path,
|
workspace_root: &Path,
|
||||||
) -> Result<OrchestrationWorktreeReady, String> {
|
) -> Result<OrchestrationWorktreeReady, String> {
|
||||||
let layout = orchestration_worktree_layout(workspace_root);
|
let layout = resolved_orchestration_worktree_layout(workspace_root)?;
|
||||||
if !layout.path.exists() {
|
if !layout.path.exists() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"orchestration worktree is missing; cannot restore existing Pod state: {}",
|
"orchestration worktree is missing; cannot restore existing Pod state: {}",
|
||||||
|
|
@ -2503,8 +2529,9 @@ async fn orchestrator_lifecycle(
|
||||||
pod_name,
|
pod_name,
|
||||||
OrchestratorPanelStatus::Restored,
|
OrchestratorPanelStatus::Restored,
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"restored existing Pod state in orchestration worktree {}",
|
"restored existing Pod state in orchestration worktree {} on branch {}",
|
||||||
worktree.layout.path.display()
|
worktree.layout.path.display(),
|
||||||
|
worktree.layout.branch
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
.mark_reload()
|
.mark_reload()
|
||||||
|
|
@ -3406,7 +3433,15 @@ fn prepare_panel_queue_handoff(
|
||||||
queue_check_failed("root-git-user", ticket_id, &root_top_level, message)
|
queue_check_failed("root-git-user", ticket_id, &root_top_level, message)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let orchestration = orchestration_worktree_layout(&root_top_level);
|
let orchestration =
|
||||||
|
resolved_orchestration_worktree_layout(&root_top_level).map_err(|message| {
|
||||||
|
queue_check_failed(
|
||||||
|
"orchestration-branch-config",
|
||||||
|
ticket_id,
|
||||||
|
&root_top_level,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
if !orchestration.path.exists() {
|
if !orchestration.path.exists() {
|
||||||
return Err(queue_check_failed(
|
return Err(queue_check_failed(
|
||||||
"orchestration-worktree-identity",
|
"orchestration-worktree-identity",
|
||||||
|
|
@ -5219,6 +5254,94 @@ mod tests {
|
||||||
assert!(created.layout.path.join("dirty.txt").exists());
|
assert!(created.layout.path.join("dirty.txt").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_and_restore_use_configured_orchestration_branch() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let root = temp.path().join("repo");
|
||||||
|
init_test_repo(&root);
|
||||||
|
write_test_ticket_config(
|
||||||
|
&root,
|
||||||
|
r#"
|
||||||
|
[orchestration]
|
||||||
|
branch = "orchestration/custom-panel"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
run_test_git(&root, &["add", ".yoi/ticket.config.toml"]).unwrap();
|
||||||
|
run_test_git(&root, &["commit", "-m", "ticket config"]).unwrap();
|
||||||
|
|
||||||
|
let resolved = resolved_orchestration_worktree_layout(&root).unwrap();
|
||||||
|
assert_eq!(resolved.branch, "orchestration/custom-panel");
|
||||||
|
assert!(
|
||||||
|
resolved
|
||||||
|
.path
|
||||||
|
.ends_with(".worktree/orchestration/repo-orchestrator")
|
||||||
|
);
|
||||||
|
|
||||||
|
let created = ensure_orchestration_worktree(&root).unwrap();
|
||||||
|
assert_eq!(created.status, OrchestrationWorktreeStatus::Created);
|
||||||
|
assert_eq!(created.layout, resolved);
|
||||||
|
let branch =
|
||||||
|
run_test_git_output(&created.layout.path, &["branch", "--show-current"]).unwrap();
|
||||||
|
assert_eq!(branch.trim(), "orchestration/custom-panel");
|
||||||
|
|
||||||
|
let restored = prepare_orchestration_worktree_for_restore(&root).unwrap();
|
||||||
|
assert_eq!(restored.status, OrchestrationWorktreeStatus::Reused);
|
||||||
|
assert_eq!(restored.layout, created.layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_configured_orchestration_branch_is_rejected_before_git_worktree_operations() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let root = temp.path().join("repo");
|
||||||
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
|
write_test_ticket_config(
|
||||||
|
&root,
|
||||||
|
r#"
|
||||||
|
[orchestration]
|
||||||
|
branch = "orchestration/bad:branch"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = ensure_orchestration_worktree(&root).unwrap_err();
|
||||||
|
assert!(err.contains("failed to load ticket config"));
|
||||||
|
assert!(err.contains("git branch name"));
|
||||||
|
assert!(!root.join(".worktree").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restore_rejects_mismatched_configured_orchestration_branch_without_checkout() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let root = temp.path().join("repo");
|
||||||
|
init_test_repo(&root);
|
||||||
|
write_test_ticket_config(
|
||||||
|
&root,
|
||||||
|
r#"
|
||||||
|
[orchestration]
|
||||||
|
branch = "orchestration/custom-panel"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
run_test_git(&root, &["add", ".yoi/ticket.config.toml"]).unwrap();
|
||||||
|
run_test_git(&root, &["commit", "-m", "ticket config"]).unwrap();
|
||||||
|
let layout = resolved_orchestration_worktree_layout(&root).unwrap();
|
||||||
|
run_test_git(
|
||||||
|
&root,
|
||||||
|
&[
|
||||||
|
"worktree",
|
||||||
|
"add",
|
||||||
|
&layout.path.display().to_string(),
|
||||||
|
"-b",
|
||||||
|
"orchestration/other-panel",
|
||||||
|
"HEAD",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = prepare_orchestration_worktree_for_restore(&root).unwrap_err();
|
||||||
|
assert!(err.contains("expected orchestration/custom-panel"));
|
||||||
|
let branch = run_test_git_output(&layout.path, &["branch", "--show-current"]).unwrap();
|
||||||
|
assert_eq!(branch.trim(), "orchestration/other-panel");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn restore_uses_existing_orchestration_worktree_even_when_dirty() {
|
fn restore_uses_existing_orchestration_worktree_even_when_dirty() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
@ -5309,6 +5432,12 @@ mod tests {
|
||||||
assert!(layout.path.join("unrelated.txt").exists());
|
assert!(layout.path.join("unrelated.txt").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_test_ticket_config(root: &Path, content: &str) {
|
||||||
|
let config_dir = root.join(".yoi");
|
||||||
|
std::fs::create_dir_all(&config_dir).unwrap();
|
||||||
|
std::fs::write(config_dir.join("ticket.config.toml"), content).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn init_test_repo(root: &Path) {
|
fn init_test_repo(root: &Path) {
|
||||||
std::fs::create_dir_all(root).unwrap();
|
std::fs::create_dir_all(root).unwrap();
|
||||||
run_test_git(root, &["init"]).unwrap();
|
run_test_git(root, &["init"]).unwrap();
|
||||||
|
|
@ -5340,6 +5469,22 @@ mod tests {
|
||||||
run_git_command(command, "run test git")
|
run_git_command(command, "run test git")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_test_git_output(root: &Path, args: &[&str]) -> Result<String, String> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(root)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.map_err(|error| format!("could not run test git: {error}"))?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"git failed to run test git: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr).trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
use crate::pod_list::{LivePodInfo, PodEntrySummary, StoredMetadataState, StoredPodInfo};
|
use crate::pod_list::{LivePodInfo, PodEntrySummary, StoredMetadataState, StoredPodInfo};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user