ticket: move ticket settings into workspace config
This commit is contained in:
@@ -12,10 +12,10 @@ pub const WORKSPACE_IDENTITY_RELATIVE_PATH: &str = ".yoi/workspace.toml";
|
||||
|
||||
/// Stable local Workspace identity persisted as a tracked, safe project record.
|
||||
///
|
||||
/// The v0 TOML schema intentionally contains identity metadata only:
|
||||
/// `workspace_id`, `created_at`, and `display_name`. Unknown fields are rejected
|
||||
/// instead of preserved because this loader cannot safely round-trip future local
|
||||
/// runtime settings without risking accidental path or secret persistence.
|
||||
/// The v0 TOML schema contains identity metadata plus optional tracked project
|
||||
/// policy tables such as `[ticket]`. Runtime/local-only settings remain rejected
|
||||
/// here because this loader cannot safely round-trip future local runtime settings
|
||||
/// without risking accidental path or secret persistence.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkspaceIdentity {
|
||||
pub workspace_id: String,
|
||||
@@ -29,6 +29,8 @@ struct WorkspaceIdentityFile {
|
||||
workspace_id: String,
|
||||
created_at: String,
|
||||
display_name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
ticket: Option<toml::Value>,
|
||||
}
|
||||
|
||||
impl WorkspaceIdentity {
|
||||
@@ -112,6 +114,7 @@ impl WorkspaceIdentity {
|
||||
workspace_id: self.workspace_id.clone(),
|
||||
created_at: self.created_at.clone(),
|
||||
display_name: self.display_name.clone(),
|
||||
ticket: None,
|
||||
})
|
||||
.map_err(|error| {
|
||||
workspace_identity_error(path, format!("failed to encode TOML: {error}"))
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use project_record::validate_record_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ticket::config::TicketConfig;
|
||||
use ticket::{LocalTicketBackend, TicketFilter, TicketIdOrSlug};
|
||||
|
||||
use crate::{Error, Result};
|
||||
@@ -17,13 +18,16 @@ pub struct LocalProjectRecordReader {
|
||||
}
|
||||
|
||||
impl LocalProjectRecordReader {
|
||||
pub fn new(workspace_root: impl Into<PathBuf>) -> Self {
|
||||
pub fn new(workspace_root: impl Into<PathBuf>) -> Result<Self> {
|
||||
let workspace_root = workspace_root.into();
|
||||
let ticket_root = workspace_root.join(".yoi/tickets");
|
||||
Self {
|
||||
let ticket_config = TicketConfig::load_workspace(&workspace_root)
|
||||
.map_err(|error| Error::Config(format!("load Ticket workspace settings: {error}")))?;
|
||||
let ticket_backend = LocalTicketBackend::new(ticket_config.backend_root().to_path_buf())
|
||||
.with_record_language(ticket_config.ticket_record_language());
|
||||
Ok(Self {
|
||||
workspace_root,
|
||||
ticket_backend: LocalTicketBackend::new(ticket_root),
|
||||
}
|
||||
ticket_backend,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn workspace_root(&self) -> &Path {
|
||||
@@ -297,7 +301,7 @@ mod tests {
|
||||
write_ticket(dir.path(), "00000000001J2", "Read bridge", "ready");
|
||||
write_objective(dir.path(), "00000000001J3", "Control plane", "active");
|
||||
|
||||
let reader = LocalProjectRecordReader::new(dir.path());
|
||||
let reader = LocalProjectRecordReader::new(dir.path()).unwrap();
|
||||
let tickets = reader.list_tickets(20).unwrap();
|
||||
assert_eq!(tickets.record_authority, "local_yoi_project_records");
|
||||
assert_eq!(tickets.items[0].id, "00000000001J2");
|
||||
@@ -313,9 +317,41 @@ mod tests {
|
||||
let objective = reader.objective("00000000001J3").unwrap();
|
||||
assert!(objective.body.contains("Objective body"));
|
||||
}
|
||||
#[test]
|
||||
fn reads_tickets_from_workspace_settings_backend_root() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join(".yoi")).unwrap();
|
||||
fs::write(
|
||||
dir.path().join(".yoi/workspace.toml"),
|
||||
r#"
|
||||
[ticket]
|
||||
language = "Japanese"
|
||||
|
||||
[ticket.backend]
|
||||
provider = "builtin:yoi_local"
|
||||
root = "project-records/tickets"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
write_ticket_at(
|
||||
&dir.path().join("project-records/tickets"),
|
||||
"00000000001J4",
|
||||
"Configured root",
|
||||
"ready",
|
||||
);
|
||||
write_ticket(dir.path(), "00000000001J5", "Default root", "ready");
|
||||
|
||||
let reader = LocalProjectRecordReader::new(dir.path()).unwrap();
|
||||
let tickets = reader.list_tickets(20).unwrap();
|
||||
assert_eq!(tickets.items.len(), 1);
|
||||
assert_eq!(tickets.items[0].id, "00000000001J4");
|
||||
}
|
||||
fn write_ticket(root: &Path, id: &str, title: &str, state: &str) {
|
||||
let ticket_dir = root.join(".yoi/tickets").join(id);
|
||||
write_ticket_at(&root.join(".yoi/tickets"), id, title, state);
|
||||
}
|
||||
|
||||
fn write_ticket_at(ticket_root: &Path, id: &str, title: &str, state: &str) {
|
||||
let ticket_dir = ticket_root.join(id);
|
||||
fs::create_dir_all(&ticket_dir).unwrap();
|
||||
fs::write(
|
||||
ticket_dir.join("item.md"),
|
||||
|
||||
@@ -272,7 +272,7 @@ impl WorkspaceApi {
|
||||
let companion = Arc::new(CompanionConsole::disabled());
|
||||
let observation_proxy = BackendObservationProxy::new(config.runtime_event_sources.clone());
|
||||
Ok(Self {
|
||||
records: LocalProjectRecordReader::new(config.workspace_root.clone()),
|
||||
records: LocalProjectRecordReader::new(config.workspace_root.clone())?,
|
||||
config,
|
||||
store,
|
||||
runtime,
|
||||
@@ -1248,11 +1248,10 @@ async fn scoped_ticket_backend_operation(
|
||||
Json(operation): Json<TicketBackendOperation>,
|
||||
) -> ApiResult<Json<TicketBackendHttpResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
let backend = LocalTicketBackend::new(
|
||||
api.config
|
||||
.workspace_root
|
||||
.join(ticket::config::DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
|
||||
);
|
||||
let config = ticket::config::TicketConfig::load_workspace(&api.config.workspace_root)
|
||||
.map_err(|error| Error::Config(format!("load Ticket workspace settings: {error}")))?;
|
||||
let backend = LocalTicketBackend::new(config.backend_root().to_path_buf())
|
||||
.with_record_language(config.ticket_record_language());
|
||||
let response = match execute_ticket_backend_operation(&backend, operation) {
|
||||
Ok(result) => TicketBackendHttpResponse::Ok { result },
|
||||
Err(error) => TicketBackendHttpResponse::Error {
|
||||
@@ -5800,6 +5799,53 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_backend_endpoint_uses_workspace_settings_backend_root() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join(".yoi")).unwrap();
|
||||
fs::write(
|
||||
dir.path().join(".yoi/workspace.toml"),
|
||||
format!(
|
||||
"workspace_id = \"{TEST_WORKSPACE_ID}\"\ncreated_at = \"{TEST_CREATED_AT}\"\ndisplay_name = \"Endpoint Test\"\n\n[ticket]\nlanguage = \"Japanese\"\n\n[ticket.backend]\nprovider = \"builtin:yoi_local\"\nroot = \"server-tickets\"\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let api = test_api(dir.path()).await;
|
||||
|
||||
let Json(response) = scoped_ticket_backend_operation(
|
||||
State(api),
|
||||
AxumPath(ScopedWorkspacePath {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
}),
|
||||
Json(TicketBackendOperation::Create {
|
||||
input: ticket::NewTicket::new("Endpoint configured root"),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("ticket backend operation failed: {}", error.error));
|
||||
|
||||
let ticket_ref = match response {
|
||||
TicketBackendHttpResponse::Ok {
|
||||
result: ticket::TicketBackendOperationResult::TicketRef(ticket_ref),
|
||||
} => ticket_ref,
|
||||
other => panic!("unexpected ticket backend response: {other:?}"),
|
||||
};
|
||||
assert!(
|
||||
dir.path()
|
||||
.join("server-tickets")
|
||||
.join(&ticket_ref.id)
|
||||
.join("item.md")
|
||||
.is_file()
|
||||
);
|
||||
assert!(
|
||||
!dir.path()
|
||||
.join(".yoi/tickets")
|
||||
.join(&ticket_ref.id)
|
||||
.join("item.md")
|
||||
.exists()
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_api(workspace_root: impl Into<PathBuf>) -> WorkspaceApi {
|
||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||
WorkspaceApi::new_with_execution_backend(
|
||||
|
||||
Reference in New Issue
Block a user