ticket: add builtin yoi local backend provider
This commit is contained in:
parent
8f5a829833
commit
cd7e60a8dd
|
|
@ -38,7 +38,7 @@ impl TicketFeature {
|
|||
pub fn for_workspace(workspace: impl AsRef<Path>) -> Self {
|
||||
let workspace = workspace.as_ref();
|
||||
match TicketConfig::load_workspace(workspace) {
|
||||
Ok(config) => Self::new(config.backend.root),
|
||||
Ok(config) => Self::new(config.backend_root().to_path_buf()),
|
||||
Err(error) => Self {
|
||||
backend_root: workspace.join("work-items"),
|
||||
config_error: Some(error.to_string()),
|
||||
|
|
@ -214,6 +214,7 @@ mod tests {
|
|||
temp.path(),
|
||||
r#"
|
||||
[backend]
|
||||
provider = "builtin:yoi_local"
|
||||
root = "tickets"
|
||||
|
||||
[roles.coder]
|
||||
|
|
@ -260,6 +261,31 @@ profile = "inherit"
|
|||
assert!(message.contains("unknown Ticket role `operator`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_ticket_backend_provider_fails_closed() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
make_work_items(&temp.path().join("work-items"));
|
||||
write_ticket_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[backend]
|
||||
provider = "github"
|
||||
"#,
|
||||
);
|
||||
let mut pending_tools = Vec::new();
|
||||
let mut hooks = HookRegistryBuilder::default();
|
||||
let report = FeatureRegistryBuilder::new()
|
||||
.with_module(ticket_tools_feature(temp.path()))
|
||||
.install_into_pending(&mut pending_tools, &mut hooks);
|
||||
|
||||
assert!(pending_tools.is_empty());
|
||||
assert!(report.reports[0].installed_tools.is_empty());
|
||||
assert_eq!(report.reports[0].diagnostics.len(), 1);
|
||||
let message = &report.reports[0].diagnostics[0].message;
|
||||
assert!(message.contains("Ticket tools not registered"));
|
||||
assert!(message.contains("unsupported Ticket backend provider `github`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_register_ticket_tools_when_root_is_missing() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -99,23 +99,44 @@ impl TicketConfig {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketBackendConfig {
|
||||
pub kind: TicketBackendKind,
|
||||
pub provider: TicketBackendProvider,
|
||||
pub root: PathBuf,
|
||||
}
|
||||
|
||||
impl TicketBackendConfig {
|
||||
pub fn default_for_workspace(workspace_root: &Path) -> Self {
|
||||
Self {
|
||||
kind: TicketBackendKind::Local,
|
||||
provider: TicketBackendProvider::BuiltinYoiLocal,
|
||||
root: workspace_root.join("work-items"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TicketBackendKind {
|
||||
Local,
|
||||
pub enum TicketBackendProvider {
|
||||
#[serde(rename = "builtin:yoi_local")]
|
||||
BuiltinYoiLocal,
|
||||
}
|
||||
|
||||
impl TicketBackendProvider {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::BuiltinYoiLocal => "builtin:yoi_local",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"builtin:yoi_local" => Some(Self::BuiltinYoiLocal),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TicketBackendProvider {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
|
|
@ -387,7 +408,12 @@ impl RawTicketConfig {
|
|||
roles.inner.insert(role, raw_role.resolve(role));
|
||||
}
|
||||
Ok(TicketConfig {
|
||||
backend: self.backend.resolve(workspace_root),
|
||||
backend: self.backend.resolve(workspace_root).map_err(|message| {
|
||||
TicketConfigError::Invalid {
|
||||
path: path.to_path_buf(),
|
||||
message,
|
||||
}
|
||||
})?,
|
||||
roles,
|
||||
})
|
||||
}
|
||||
|
|
@ -397,18 +423,40 @@ impl RawTicketConfig {
|
|||
#[serde(deny_unknown_fields)]
|
||||
struct RawBackendConfig {
|
||||
#[serde(default)]
|
||||
kind: Option<TicketBackendKind>,
|
||||
provider: Option<String>,
|
||||
#[serde(default)]
|
||||
kind: Option<String>,
|
||||
#[serde(default)]
|
||||
root: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl RawBackendConfig {
|
||||
fn resolve(self, workspace_root: &Path) -> TicketBackendConfig {
|
||||
fn resolve(self, workspace_root: &Path) -> Result<TicketBackendConfig, String> {
|
||||
let provider = match (self.provider, self.kind) {
|
||||
(Some(provider), None) => TicketBackendProvider::parse(&provider).ok_or_else(|| {
|
||||
format!(
|
||||
"unsupported Ticket backend provider `{provider}`; supported provider: `builtin:yoi_local`"
|
||||
)
|
||||
})?,
|
||||
(None, Some(kind)) if kind == "local" => TicketBackendProvider::BuiltinYoiLocal,
|
||||
(None, Some(kind)) => {
|
||||
return Err(format!(
|
||||
"unsupported legacy Ticket backend kind `{kind}`; use provider = \"builtin:yoi_local\""
|
||||
));
|
||||
}
|
||||
(None, None) => TicketBackendProvider::BuiltinYoiLocal,
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(
|
||||
"backend.provider and legacy backend.kind are mutually exclusive; use provider = \"builtin:yoi_local\""
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let root = self.root.unwrap_or_else(|| PathBuf::from("work-items"));
|
||||
TicketBackendConfig {
|
||||
kind: self.kind.unwrap_or(TicketBackendKind::Local),
|
||||
Ok(TicketBackendConfig {
|
||||
provider,
|
||||
root: join_if_relative(workspace_root, &root),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -458,7 +506,10 @@ mod tests {
|
|||
let temp = TempDir::new().unwrap();
|
||||
let config = TicketConfig::load_workspace(temp.path()).unwrap();
|
||||
|
||||
assert_eq!(config.backend.kind, TicketBackendKind::Local);
|
||||
assert_eq!(
|
||||
config.backend.provider,
|
||||
TicketBackendProvider::BuiltinYoiLocal
|
||||
);
|
||||
assert_eq!(config.backend.root, temp.path().join("work-items"));
|
||||
for role in TicketRole::ALL {
|
||||
let role_config = config.role(role);
|
||||
|
|
@ -475,7 +526,7 @@ mod tests {
|
|||
temp.path(),
|
||||
r#"
|
||||
[backend]
|
||||
kind = "local"
|
||||
provider = "builtin:yoi_local"
|
||||
root = "custom-work-items"
|
||||
|
||||
[roles.intake]
|
||||
|
|
@ -506,6 +557,10 @@ workflow = "ticket-orchestrator-routing"
|
|||
);
|
||||
|
||||
let config = TicketConfig::load_workspace(temp.path()).unwrap();
|
||||
assert_eq!(
|
||||
config.backend.provider,
|
||||
TicketBackendProvider::BuiltinYoiLocal
|
||||
);
|
||||
assert_eq!(config.backend.root, temp.path().join("custom-work-items"));
|
||||
assert_eq!(
|
||||
config.profile_for(TicketRole::Intake).as_str(),
|
||||
|
|
@ -576,7 +631,47 @@ system_instruction = "$workspace/not-supported"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_backend_kind_is_rejected() {
|
||||
fn legacy_backend_kind_local_is_transitional_alias() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[backend]
|
||||
kind = "local"
|
||||
root = "legacy-work-items"
|
||||
"#,
|
||||
);
|
||||
|
||||
let config = TicketConfig::load_workspace(temp.path()).unwrap();
|
||||
assert_eq!(
|
||||
config.backend.provider,
|
||||
TicketBackendProvider::BuiltinYoiLocal
|
||||
);
|
||||
assert_eq!(config.backend_root(), temp.path().join("legacy-work-items"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_backend_provider_is_rejected() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[backend]
|
||||
provider = "github"
|
||||
"#,
|
||||
);
|
||||
|
||||
let error = TicketConfig::load_workspace(temp.path()).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("unsupported Ticket backend provider `github`")
|
||||
);
|
||||
assert!(error.to_string().contains("builtin:yoi_local"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_legacy_backend_kind_is_rejected() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
|
|
@ -587,8 +682,31 @@ kind = "github"
|
|||
);
|
||||
|
||||
let error = TicketConfig::load_workspace(temp.path()).unwrap_err();
|
||||
assert!(error.to_string().contains("unknown variant"));
|
||||
assert!(error.to_string().contains("github"));
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("unsupported legacy Ticket backend kind `github`")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_provider_and_legacy_kind_are_mutually_exclusive() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[backend]
|
||||
provider = "builtin:yoi_local"
|
||||
kind = "local"
|
||||
"#,
|
||||
);
|
||||
|
||||
let error = TicketConfig::load_workspace(temp.path()).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("backend.provider and legacy backend.kind are mutually exclusive")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -745,7 +745,7 @@ fn default_author() -> String {
|
|||
}
|
||||
|
||||
fn help_text() -> &'static str {
|
||||
"yoi ticket\n\nUsage:\n yoi ticket create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]\n yoi ticket list [--status open|pending|closed|all]\n yoi ticket show <id-or-slug>\n yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket review <id-or-slug> (--approve|--request-changes) (--file <path>|--message <text>)\n yoi ticket status <id-or-slug> <open|pending|closed>\n yoi ticket close <id-or-slug> (--resolution <text>|--file <path>)\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nBackend:\n Uses the workspace Ticket config at .yoi/ticket.config.toml when present.\n Without config, the local backend root is <cwd>/work-items.\n"
|
||||
"yoi ticket\n\nUsage:\n yoi ticket create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]\n yoi ticket list [--status open|pending|closed|all]\n yoi ticket show <id-or-slug>\n yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket review <id-or-slug> (--approve|--request-changes) (--file <path>|--message <text>)\n yoi ticket status <id-or-slug> <open|pending|closed>\n yoi ticket close <id-or-slug> (--resolution <text>|--file <path>)\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nBackend:\n Uses the workspace Ticket config at .yoi/ticket.config.toml when present.\n Supported provider: builtin:yoi_local.\n Without config, the transitional local backend root is <cwd>/work-items.\n"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -867,7 +867,7 @@ mod tests {
|
|||
fs::create_dir_all(temp.path().join(".yoi")).unwrap();
|
||||
fs::write(
|
||||
temp.path().join(".yoi/ticket.config.toml"),
|
||||
"[backend]\nroot = \"custom-work-items\"\n",
|
||||
"[backend]\nprovider = \"builtin:yoi_local\"\nroot = \"custom-work-items\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ MVP shape:
|
|||
|
||||
```toml
|
||||
[backend]
|
||||
kind = "local"
|
||||
provider = "builtin:yoi_local"
|
||||
root = "work-items"
|
||||
|
||||
[roles.intake]
|
||||
|
|
@ -103,9 +103,12 @@ This is not an arbitrary role registry. The fixed roles are the roles required b
|
|||
|
||||
`workflow` is the workflow the launched role should follow. Workflow state and phase-specific prompt injection are future work; any dynamic prompt content must be committed as history before it affects model context.
|
||||
|
||||
`provider = "builtin:yoi_local"` selects Yoi's built-in local Ticket backend. `root = "work-items"` is the active transitional root until the planned storage migration moves records under `.yoi/`. Legacy `kind = "local"` is accepted only as a short transitional alias; new configs should use `provider`.
|
||||
|
||||
If `.yoi/ticket.config.toml` is missing, defaults are:
|
||||
|
||||
- backend: local `<workspace>/work-items`
|
||||
- backend provider: `builtin:yoi_local`
|
||||
- backend root: transitional `<workspace>/work-items`
|
||||
- all role profiles: `inherit`
|
||||
- no launch prompt refs
|
||||
- workflows:
|
||||
|
|
@ -276,7 +279,7 @@ Because top-level TUI role launches cannot inherit a parent Profile, configure c
|
|||
# .yoi/ticket.config.toml
|
||||
|
||||
[backend]
|
||||
kind = "local"
|
||||
provider = "builtin:yoi_local"
|
||||
root = "work-items"
|
||||
|
||||
[roles.intake]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user