ticket: move local storage to .yoi/tickets

This commit is contained in:
2026-06-06 06:44:10 +09:00
parent 749b3f3aee
commit da94b1ec50
529 changed files with 98 additions and 156 deletions
+16 -9
View File
@@ -14,6 +14,8 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const TICKET_CONFIG_RELATIVE_PATH: &str = ".yoi/ticket.config.toml";
/// Workspace-relative default root for the built-in local Ticket backend.
pub const DEFAULT_TICKET_BACKEND_RELATIVE_PATH: &str = ".yoi/tickets";
#[derive(Debug, Error)]
pub enum TicketConfigError {
@@ -107,7 +109,7 @@ impl TicketBackendConfig {
pub fn default_for_workspace(workspace_root: &Path) -> Self {
Self {
provider: TicketBackendProvider::BuiltinYoiLocal,
root: workspace_root.join("work-items"),
root: workspace_root.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
}
}
}
@@ -452,7 +454,9 @@ impl RawBackendConfig {
);
}
};
let root = self.root.unwrap_or_else(|| PathBuf::from("work-items"));
let root = self
.root
.unwrap_or_else(|| PathBuf::from(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
Ok(TicketBackendConfig {
provider,
root: join_if_relative(workspace_root, &root),
@@ -510,7 +514,10 @@ mod tests {
config.backend.provider,
TicketBackendProvider::BuiltinYoiLocal
);
assert_eq!(config.backend.root, temp.path().join("work-items"));
assert_eq!(
config.backend.root,
temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)
);
for role in TicketRole::ALL {
let role_config = config.role(role);
assert_eq!(role_config.profile.as_str(), "inherit");
@@ -527,7 +534,7 @@ mod tests {
r#"
[backend]
provider = "builtin:yoi_local"
root = "custom-work-items"
root = "custom-tickets"
[roles.intake]
profile = "project:intake"
@@ -561,7 +568,7 @@ workflow = "ticket-orchestrator-routing"
config.backend.provider,
TicketBackendProvider::BuiltinYoiLocal
);
assert_eq!(config.backend.root, temp.path().join("custom-work-items"));
assert_eq!(config.backend.root, temp.path().join("custom-tickets"));
assert_eq!(
config.profile_for(TicketRole::Intake).as_str(),
"project:intake"
@@ -638,7 +645,7 @@ system_instruction = "$workspace/not-supported"
r#"
[backend]
kind = "local"
root = "legacy-work-items"
root = "legacy-tickets"
"#,
);
@@ -647,7 +654,7 @@ root = "legacy-work-items"
config.backend.provider,
TicketBackendProvider::BuiltinYoiLocal
);
assert_eq!(config.backend_root(), temp.path().join("legacy-work-items"));
assert_eq!(config.backend_root(), temp.path().join("legacy-tickets"));
}
#[test]
@@ -716,12 +723,12 @@ kind = "local"
temp.path(),
r#"
[backend]
root = "nested/work-items"
root = "nested/tickets"
"#,
);
let config = TicketConfig::load_workspace(temp.path()).unwrap();
assert_eq!(config.backend_root(), temp.path().join("nested/work-items"));
assert_eq!(config.backend_root(), temp.path().join("nested/tickets"));
}
#[test]
+11 -11
View File
@@ -1,7 +1,7 @@
//! Ticket domain types and the local `work-items/` file backend.
//! Ticket domain types and the local `.yoi/tickets/` file backend.
//!
//! The public domain name is **Ticket**. `LocalTicketBackend` preserves the
//! repository's current `work-items/{open,pending,closed}/<id>/` layout and the
//! repository's current `.yoi/tickets/{open,pending,closed}/<id>/` layout and the
//! event format used by `tickets.sh` while exposing typed Rust operations.
use std::collections::{BTreeMap, BTreeSet, HashMap};
@@ -1525,7 +1525,7 @@ mod tests {
use tempfile::TempDir;
fn backend(dir: &TempDir) -> LocalTicketBackend {
LocalTicketBackend::new(dir.path().join("work-items"))
LocalTicketBackend::new(dir.path().join("tickets"))
}
fn script_path() -> PathBuf {
@@ -1592,12 +1592,12 @@ action_required: none
let mut input = NewTicket::new("Example Ticket");
input.labels = vec!["ticket".into(), "backend".into()];
let ticket = backend.create(input).unwrap();
let dir = tmp.path().join("work-items/open").join(&ticket.id);
let dir = tmp.path().join("tickets/open").join(&ticket.id);
assert!(dir.join("item.md").exists());
assert!(dir.join("thread.md").exists());
assert!(dir.join("artifacts/.gitkeep").exists());
assert_eq!(ticket.slug, "example-ticket");
assert_script_ok(&tmp.path().join("work-items"), &["doctor"]);
assert_script_ok(&tmp.path().join("tickets"), &["doctor"]);
}
#[test]
@@ -1622,7 +1622,7 @@ action_required: none
.unwrap();
let pending_item = tmp
.path()
.join("work-items/pending")
.join("tickets/pending")
.join(&ticket.id)
.join("item.md");
assert!(pending_item.exists());
@@ -1632,19 +1632,19 @@ action_required: none
MarkdownText::new("Done.\n"),
)
.unwrap();
let closed_dir = tmp.path().join("work-items/closed").join(&ticket.id);
let closed_dir = tmp.path().join("tickets/closed").join(&ticket.id);
assert!(closed_dir.join("resolution.md").exists());
let thread = fs::read_to_string(closed_dir.join("thread.md")).unwrap();
assert!(thread.contains("<!-- event: review"));
assert!(thread.contains("status: approve"));
assert!(thread.contains("<!-- event: close"));
assert_script_ok(&tmp.path().join("work-items"), &["doctor"]);
assert_script_ok(&tmp.path().join("tickets"), &["doctor"]);
}
#[test]
fn reads_ticket_created_by_tickets_sh_and_script_mutates_rust_ticket() {
let tmp = TempDir::new().unwrap();
let work_items = tmp.path().join("work-items");
let work_items = tmp.path().join("tickets");
let id = assert_script_ok(
&work_items,
&[
@@ -1676,7 +1676,7 @@ action_required: none
#[test]
fn doctor_reports_core_consistency_errors() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("work-items");
let root = tmp.path().join("tickets");
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
fs::write(
root.join("open/bad/item.md"),
@@ -1733,7 +1733,7 @@ action_required: none
#[test]
fn rejects_unsafe_components_for_status_moves() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("work-items");
let root = tmp.path().join("tickets");
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
fs::write(
root.join("open/bad/item.md"),
+2 -2
View File
@@ -41,7 +41,7 @@ pub const TICKET_TOOL_NAMES: [&str; 8] = [
];
const CREATE_DESCRIPTION: &str = "Create a Ticket through the configured typed Ticket backend. \
Inputs mirror the work-items item.md fields; `title` is required, `body` is Markdown, and the \
Inputs mirror the Ticket `item.md` fields; `title` is required, `body` is Markdown, and the \
backend assigns the id and writes tickets.sh-compatible files under the configured backend root.";
const LIST_DESCRIPTION: &str = "List Tickets from the configured typed Ticket backend. Filter by \
status (`open`, `pending`, `closed`, or `all`) and optionally kind/priority/label. Output is a \
@@ -778,7 +778,7 @@ mod tests {
use tempfile::TempDir;
fn backend(temp: &TempDir) -> LocalTicketBackend {
LocalTicketBackend::new(temp.path().join("work-items"))
LocalTicketBackend::new(temp.path().join("tickets"))
}
fn tool(definition: ToolDefinition) -> Arc<dyn Tool> {