fix: remove legacy ticket conclusion reason limits
This commit is contained in:
+89
-15
@@ -26,7 +26,6 @@ pub mod tool;
|
||||
|
||||
pub use sqlite_schema::{migrate_sqlite_ticket_schema, verify_sqlite_ticket_schema};
|
||||
|
||||
const MAX_STATE_CHANGE_REASON_BYTES: usize = 1024;
|
||||
const MAX_INTAKE_SUMMARY_BODY_BYTES: usize = 16 * 1024;
|
||||
const MAX_ORCHESTRATION_PLAN_TEXT_BYTES: usize = 16 * 1024;
|
||||
const MAX_ORCHESTRATION_PLAN_FIELD_BYTES: usize = 1024;
|
||||
@@ -4293,11 +4292,12 @@ fn validate_required_event_value(label: &str, value: &str) -> Result<()> {
|
||||
fn validate_state_change(change: &TicketStateChange) -> Result<()> {
|
||||
validate_required_event_value("from", &change.from)?;
|
||||
validate_required_event_value("to", &change.to)?;
|
||||
validate_required_event_value("reason", &change.reason)?;
|
||||
if change.reason.len() > MAX_STATE_CHANGE_REASON_BYTES {
|
||||
return Err(TicketError::Conflict(format!(
|
||||
"state_changed reason exceeds {MAX_STATE_CHANGE_REASON_BYTES} bytes"
|
||||
)));
|
||||
// Reasons are persisted as SQLite text, not legacy thread-comment attributes.
|
||||
// Preserve free-form Markdown without per-field length or single-line limits.
|
||||
if change.reason.trim().is_empty() {
|
||||
return Err(TicketError::Conflict(
|
||||
"state_changed event requires non-empty reason".to_owned(),
|
||||
));
|
||||
}
|
||||
if let Some(author) = change.author.as_deref() {
|
||||
validate_required_event_value("author", author)?;
|
||||
@@ -4306,11 +4306,6 @@ fn validate_state_change(change: &TicketStateChange) -> Result<()> {
|
||||
validate_required_event_value("reference_kind", &reference.kind)?;
|
||||
validate_required_event_value("reference_target", &reference.target)?;
|
||||
}
|
||||
if change.body.as_str().len() > MAX_INTAKE_SUMMARY_BODY_BYTES {
|
||||
return Err(TicketError::Conflict(format!(
|
||||
"state_changed body exceeds {MAX_INTAKE_SUMMARY_BODY_BYTES} bytes"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6221,6 +6216,89 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_preserves_long_and_multiline_reasons_across_reload_and_replay() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let db = tmp.path().join("tickets.db");
|
||||
let backend = SqliteTicketBackend::open(&db, "workspace-test").unwrap();
|
||||
for reason in [
|
||||
"x".repeat(1025),
|
||||
"日本語の完了理由を省略せず記録する。".repeat(1024),
|
||||
"## 完了理由\r\n\n- 検証済み\n<!-- 詳細 -->\n".repeat(1024),
|
||||
] {
|
||||
let (reference, ticket) = backend
|
||||
.create_with_snapshot(NewTicket::new("Long completion reason"))
|
||||
.unwrap();
|
||||
let request = TicketCompletion {
|
||||
reason: reason.clone(),
|
||||
..completion_request(&ticket, "complete")
|
||||
};
|
||||
let completed = backend.complete(&reference.id, request.clone()).unwrap();
|
||||
assert_eq!(completed.meta.workflow_state, TicketWorkflowState::Done);
|
||||
assert_eq!(completed.events.len(), ticket.events.len() + 1);
|
||||
let event = completed.events.last().unwrap();
|
||||
assert_eq!(event.reason.as_deref(), Some(reason.as_str()));
|
||||
assert_eq!(event.body.as_str(), reason);
|
||||
|
||||
let reopened = SqliteTicketBackend::open(&db, "workspace-test").unwrap();
|
||||
assert_eq!(
|
||||
reopened.show(reference.id.clone().into()).unwrap(),
|
||||
completed
|
||||
);
|
||||
assert_eq!(
|
||||
reopened.complete(&reference.id, request.clone()).unwrap(),
|
||||
completed
|
||||
);
|
||||
let changed = TicketCompletion {
|
||||
reason: format!("{reason}追加の判断"),
|
||||
..request
|
||||
};
|
||||
assert!(matches!(
|
||||
reopened.complete(&reference.id, changed),
|
||||
Err(TicketError::OperationFingerprintMismatch { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_changes_and_close_preserve_long_markdown_reasons_and_bodies() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let backend = backend(&tmp);
|
||||
let (reference, _) = backend
|
||||
.create_with_snapshot(NewTicket::new("Long state and close reasons"))
|
||||
.unwrap();
|
||||
let reason = "## 判断\n\n長文の理由を記録する。\n".repeat(1024);
|
||||
let body = "## 検証結果\n\n本文も省略しない。\n".repeat(1024);
|
||||
backend
|
||||
.set_workflow_state(
|
||||
reference.id.clone().into(),
|
||||
TicketStateChange::new("planning", "inprogress", reason.clone(), body.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let current = backend.show(reference.id.clone().into()).unwrap();
|
||||
let event = current.events.last().unwrap();
|
||||
assert_eq!(event.reason.as_deref(), Some(reason.as_str()));
|
||||
assert_eq!(event.body.as_str(), body);
|
||||
let request = TicketStateUpdate {
|
||||
state: TicketWorkflowState::Closed,
|
||||
reason: reason.clone(),
|
||||
..completion_request(¤t, "close").into()
|
||||
};
|
||||
let closed = backend
|
||||
.update_state(&reference.id, request.clone())
|
||||
.unwrap();
|
||||
assert_eq!(closed.meta.workflow_state, TicketWorkflowState::Closed);
|
||||
assert_eq!(closed.resolution, Some(MarkdownText::new(&reason)));
|
||||
assert_eq!(
|
||||
closed.events.last().unwrap().reason.as_deref(),
|
||||
Some(reason.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
backend.update_state(&reference.id, request).unwrap(),
|
||||
closed
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_cas_rejects_stale_item_or_state_without_mutation() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
@@ -6565,10 +6643,6 @@ mod tests {
|
||||
}],
|
||||
..request.clone()
|
||||
},
|
||||
TicketCompletion {
|
||||
reason: "x".repeat(MAX_STATE_CHANGE_REASON_BYTES + 1),
|
||||
..request.clone()
|
||||
},
|
||||
TicketCompletion {
|
||||
author: Some("".to_owned()),
|
||||
..request.clone()
|
||||
|
||||
@@ -48049,7 +48049,8 @@ mod tests {
|
||||
operation_key: "research-done".into(),
|
||||
expected_item_revision: ticket::ticket_item_revision(&before),
|
||||
expected_state: TicketWorkflowState::InProgress,
|
||||
reason: "Investigation findings recorded in the Ticket thread".into(),
|
||||
reason: "## 完了理由\n\n日本語の長い検証結果を省略せず記録する。\n<!-- 完了 -->\n"
|
||||
.repeat(1024),
|
||||
references: Vec::new(),
|
||||
};
|
||||
let call = |request| {
|
||||
@@ -48062,6 +48063,13 @@ mod tests {
|
||||
};
|
||||
let Json(completed) = call(request.clone()).await.unwrap();
|
||||
assert_eq!(completed.meta.workflow_state, TicketWorkflowState::Done);
|
||||
let event = completed.events.last().unwrap();
|
||||
assert_eq!(event.reason.as_deref(), Some(request.reason.as_str()));
|
||||
assert_eq!(event.body.as_str(), request.reason);
|
||||
assert_eq!(
|
||||
backend.show(reference.id.clone().into()).unwrap(),
|
||||
completed
|
||||
);
|
||||
assert!(
|
||||
merge_request_store(&api, TEST_WORKSPACE_ID)
|
||||
.unwrap()
|
||||
|
||||
Reference in New Issue
Block a user