feat: unify Ticket posts and simplify detail layout
Checkpoint requested before the targets read-tool correction. Includes canonical initial posts, creator/edit metadata, migration and UI work, and removal of delivery diagnostics from Ticket details.\n\nIntegration remains incomplete: the Client post-edit trait implementation and server post routes/dispatch are not yet connected; cargo check currently fails. This commit does not assert validation or delivery completion.
This commit is contained in:
@@ -0,0 +1,525 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use tempfile::TempDir;
|
||||
use ticket::{
|
||||
MarkdownText, NewTicket, NewTicketEvent, SqliteTicketBackend, TicketBackend, TicketEventKind,
|
||||
TicketItemEdit, TicketPostEdit, migrate_sqlite_ticket_schema,
|
||||
migrate_sqlite_ticket_schema_in_transaction, ticket_content_digest,
|
||||
verify_sqlite_ticket_schema,
|
||||
};
|
||||
|
||||
fn comment(body: &str, author: &str) -> NewTicketEvent {
|
||||
let mut event = NewTicketEvent::new(TicketEventKind::Comment, body);
|
||||
event.author = Some(author.into());
|
||||
event
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_and_discussion_comments_share_identity_authorship_and_cas_edits() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let path = temp.path().join("posts.db");
|
||||
let backend = SqliteTicketBackend::open(&path, "workspace").unwrap();
|
||||
let mut input = NewTicket::new("Real first post");
|
||||
input.body = MarkdownText::new("First authored post");
|
||||
input.author = Some("creator".into());
|
||||
let (reference, created) = backend.create_with_snapshot(input).unwrap();
|
||||
assert_eq!(created.events.len(), 1);
|
||||
let initial = &created.events[0];
|
||||
assert_eq!(initial.kind, TicketEventKind::Comment);
|
||||
assert!(initial.is_initial);
|
||||
assert_eq!(initial.body, created.document.body);
|
||||
assert_eq!(initial.author.as_deref(), Some("creator"));
|
||||
assert_eq!(initial.at, created.meta.created_at);
|
||||
assert_eq!(created.meta.created_by.as_deref(), Some("creator"));
|
||||
assert!(initial.edited_at.is_none());
|
||||
backend
|
||||
.add_event(
|
||||
reference.id.clone().into(),
|
||||
comment("Discussion", "commenter"),
|
||||
)
|
||||
.unwrap();
|
||||
let before = backend.show(reference.id.clone().into()).unwrap();
|
||||
let ordinary = before.events[1].clone();
|
||||
assert!(!ordinary.is_initial);
|
||||
assert_eq!(ordinary.kind, initial.kind);
|
||||
let initial_time = initial.at.clone();
|
||||
for original in [ordinary, initial.clone()] {
|
||||
let edit = TicketPostEdit {
|
||||
event_ref: original.event_ref.clone(),
|
||||
expected_body: original.body.clone(),
|
||||
body: MarkdownText::new(format!("Edited {}", original.event_ref)),
|
||||
author: "editor".into(),
|
||||
};
|
||||
let (previous, current) = backend
|
||||
.edit_post_with_snapshots(reference.id.clone().into(), edit.clone())
|
||||
.unwrap();
|
||||
let post = current
|
||||
.events
|
||||
.iter()
|
||||
.find(|p| p.event_ref == original.event_ref)
|
||||
.unwrap();
|
||||
assert_eq!(post.author, original.author);
|
||||
assert_eq!(post.at, original.at);
|
||||
assert_eq!(post.edited_by.as_deref(), Some("editor"));
|
||||
assert!(post.edited_at.is_some());
|
||||
assert_eq!(current.meta.created_by.as_deref(), Some("creator"));
|
||||
assert_eq!(current.meta.created_at, initial_time);
|
||||
assert_eq!(current.events.len(), 2);
|
||||
if original.is_initial {
|
||||
assert_eq!(current.document.body, edit.body);
|
||||
assert_ne!(
|
||||
ticket_content_digest(&previous),
|
||||
ticket_content_digest(¤t)
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
ticket_content_digest(&previous),
|
||||
ticket_content_digest(¤t)
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
backend
|
||||
.edit_post(reference.id.clone().into(), edit)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(backend.show(reference.id.clone().into()).unwrap(), current);
|
||||
}
|
||||
let previous = backend.show(reference.id.clone().into()).unwrap();
|
||||
let edited = backend
|
||||
.edit_item(
|
||||
reference.id.clone().into(),
|
||||
TicketItemEdit {
|
||||
title: Some("New title".into()),
|
||||
body: Some(MarkdownText::new("Item authoring edits initial post")),
|
||||
author: Some("item-editor".into()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(edited.events.len(), previous.events.len());
|
||||
assert_eq!(edited.events[0].author, previous.events[0].author);
|
||||
assert_eq!(edited.events[0].at, previous.events[0].at);
|
||||
assert_eq!(edited.events[0].edited_by.as_deref(), Some("item-editor"));
|
||||
assert_eq!(edited.document.body, edited.events[0].body);
|
||||
assert!(
|
||||
edited
|
||||
.events
|
||||
.iter()
|
||||
.all(|e| !matches!(e.kind.as_str(), "create" | "item_edit"))
|
||||
);
|
||||
let reopened = SqliteTicketBackend::open(&path, "workspace").unwrap();
|
||||
assert_eq!(reopened.show(reference.id.into()).unwrap(), edited);
|
||||
let conn = Connection::open(path).unwrap();
|
||||
assert_eq!(
|
||||
conn.query_row(
|
||||
"SELECT count(*) FROM pragma_table_info('typed_tickets') WHERE name='body'",
|
||||
[],
|
||||
|r| r.get::<_, i64>(0)
|
||||
)
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_rejects_system_judgments_other_tickets_and_missing_identity() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let backend = SqliteTicketBackend::open(temp.path().join("posts.db"), "workspace").unwrap();
|
||||
let first = backend.create(NewTicket::new("first")).unwrap();
|
||||
let second = backend.create(NewTicket::new("second")).unwrap();
|
||||
let mut event = comment("System judgment", "system");
|
||||
event.kind = TicketEventKind::Decision;
|
||||
backend.add_event(first.id.clone().into(), event).unwrap();
|
||||
let ticket = backend.show(first.id.clone().into()).unwrap();
|
||||
let system = &ticket.events[1];
|
||||
let edit = TicketPostEdit {
|
||||
event_ref: system.event_ref.clone(),
|
||||
expected_body: system.body.clone(),
|
||||
body: MarkdownText::new("changed"),
|
||||
author: "editor".into(),
|
||||
};
|
||||
assert!(
|
||||
backend
|
||||
.edit_post(first.id.clone().into(), edit.clone())
|
||||
.is_err()
|
||||
);
|
||||
assert!(backend.edit_post(second.id.into(), edit).is_err());
|
||||
let initial = &ticket.events[0];
|
||||
assert!(
|
||||
backend
|
||||
.edit_post(
|
||||
first.id.clone().into(),
|
||||
TicketPostEdit {
|
||||
event_ref: initial.event_ref.clone(),
|
||||
expected_body: initial.body.clone(),
|
||||
body: MarkdownText::new("changed"),
|
||||
author: " ".into()
|
||||
}
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(backend.show(first.id.into()).unwrap(), ticket);
|
||||
}
|
||||
|
||||
fn schema11(conn: &Connection) {
|
||||
// Build the actual retained component shape, not a current schema with its marker changed.
|
||||
conn.execute_batch(include_str!("../src/latest_schema.sql"))
|
||||
.unwrap();
|
||||
conn.execute_batch(include_str!("../src/assignment_schema.sql"))
|
||||
.unwrap();
|
||||
conn.execute_batch(include_str!("../src/assignment_tracking_schema.sql"))
|
||||
.unwrap();
|
||||
conn.execute_batch("CREATE TABLE ticket_schema_migrations(version INTEGER PRIMARY KEY,name TEXT NOT NULL,applied_at TEXT NOT NULL); INSERT INTO ticket_schema_migrations VALUES(11,'ticket schema baseline','original-marker');").unwrap();
|
||||
}
|
||||
|
||||
fn seed_legacy(conn: &Connection, id: &str) {
|
||||
conn.execute("INSERT INTO typed_tickets(workspace_id,ticket_id,slug,title,status,kind,priority,body,created_at,workflow_state,workflow_state_explicit) VALUES('workspace',?1,?1,'Legacy','open','task','P2','Latest body','ticket-time','open',1)", [id]).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema12_migrates_body_authors_archived_history_and_stable_references_then_replays() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let path = temp.path().join("legacy.db");
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
conn.execute_batch("PRAGMA foreign_keys=ON").unwrap();
|
||||
schema11(&conn);
|
||||
for id in ["known", "unknown", "metadata"] {
|
||||
seed_legacy(&conn, id);
|
||||
}
|
||||
conn.execute_batch("INSERT INTO typed_ticket_events(workspace_id,ticket_id,event_index,kind,author,at,body) VALUES
|
||||
('workspace','known',0,'create','original-author','original-time','Created'),
|
||||
('workspace','known',1,'comment','discussant','discussion-time','Discussion'),
|
||||
('workspace','known',2,'item_edit','legacy-editor','edit-time','Body changed');
|
||||
INSERT INTO typed_ticket_event_attributes VALUES('workspace','known',1,'event_id','external-original-id');
|
||||
INSERT INTO typed_ticket_event_attributes VALUES('workspace','known',2,'body_edit','whole');
|
||||
INSERT INTO typed_ticket_event_references VALUES('workspace','known',2,0,'commit','original-evidence');
|
||||
INSERT INTO typed_ticket_raw_frontmatter VALUES('workspace','metadata','created_by','metadata-author');").unwrap();
|
||||
migrate_sqlite_ticket_schema(&conn).unwrap();
|
||||
verify_sqlite_ticket_schema(&conn).unwrap();
|
||||
let backend = SqliteTicketBackend::open_verified(&path, "workspace").unwrap();
|
||||
let known = backend.show("known".into()).unwrap();
|
||||
assert_eq!(known.meta.created_at.as_deref(), Some("ticket-time"));
|
||||
assert_eq!(known.meta.created_by.as_deref(), Some("original-author"));
|
||||
assert_eq!(known.events.len(), 2);
|
||||
assert_eq!(known.events[0].author.as_deref(), Some("original-author"));
|
||||
assert_eq!(known.events[0].at.as_deref(), Some("original-time"));
|
||||
assert_eq!(known.document.body.as_str(), "Latest body");
|
||||
assert_eq!(known.events[0].body, known.document.body);
|
||||
assert_eq!(known.events[1].event_ref, "known:1");
|
||||
assert_eq!(known.events[1].attributes["event_id"], "known:1");
|
||||
assert_eq!(conn.query_row("SELECT value FROM typed_ticket_event_attributes WHERE ticket_id='known' AND event_index=1 AND key='event_id'", [], |r| r.get::<_,String>(0)).unwrap(), "external-original-id");
|
||||
assert_eq!(known.events[1].attributes["event_sequence"], "1");
|
||||
let unknown = backend.show("unknown".into()).unwrap();
|
||||
assert_eq!(unknown.meta.created_by, None);
|
||||
assert_eq!(unknown.events[0].author, None);
|
||||
assert_eq!(unknown.events[0].at.as_deref(), Some("ticket-time"));
|
||||
assert_eq!(
|
||||
backend.show("metadata".into()).unwrap().events[0]
|
||||
.author
|
||||
.as_deref(),
|
||||
Some("metadata-author")
|
||||
);
|
||||
let history = || {
|
||||
conn.prepare("SELECT event_index,kind,author,at,body,archived FROM typed_ticket_events WHERE ticket_id='known' AND event_index>=0 ORDER BY event_index").unwrap().query_map([], |r| Ok((r.get::<_,i64>(0)?,r.get::<_,String>(1)?,r.get::<_,String>(2)?,r.get::<_,String>(3)?,r.get::<_,String>(4)?,r.get::<_,bool>(5)?))).unwrap().collect::<rusqlite::Result<Vec<_>>>().unwrap()
|
||||
};
|
||||
let original_history = history();
|
||||
assert_eq!(
|
||||
original_history[0],
|
||||
(
|
||||
0,
|
||||
"create".into(),
|
||||
"original-author".into(),
|
||||
"original-time".into(),
|
||||
"Created".into(),
|
||||
true
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
original_history[2],
|
||||
(
|
||||
2,
|
||||
"item_edit".into(),
|
||||
"legacy-editor".into(),
|
||||
"edit-time".into(),
|
||||
"Body changed".into(),
|
||||
true
|
||||
)
|
||||
);
|
||||
assert_eq!(conn.query_row("SELECT target FROM typed_ticket_event_references WHERE ticket_id='known' AND event_index=2", [], |r| r.get::<_,String>(0)).unwrap(), "original-evidence");
|
||||
migrate_sqlite_ticket_schema(&conn).unwrap();
|
||||
assert_eq!(history(), original_history);
|
||||
assert_eq!(backend.show("known".into()).unwrap(), known);
|
||||
assert_eq!(
|
||||
conn.query_row(
|
||||
"SELECT applied_at FROM ticket_schema_migrations WHERE version=11",
|
||||
[],
|
||||
|r| r.get::<_, String>(0)
|
||||
)
|
||||
.unwrap(),
|
||||
"original-marker"
|
||||
);
|
||||
assert_eq!(
|
||||
conn.query_row(
|
||||
"SELECT max(version) FROM ticket_schema_migrations",
|
||||
[],
|
||||
|r| r.get::<_, i64>(0)
|
||||
)
|
||||
.unwrap(),
|
||||
12
|
||||
);
|
||||
backend
|
||||
.add_event("known".into(), comment("Next", "author"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
backend
|
||||
.show("known".into())
|
||||
.unwrap()
|
||||
.events
|
||||
.last()
|
||||
.unwrap()
|
||||
.event_ref,
|
||||
"known:3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema12_reserved_index_collision_rolls_back_component_and_host_can_retry() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
schema11(&conn);
|
||||
seed_legacy(&conn, "collision");
|
||||
conn.execute("INSERT INTO typed_ticket_events(workspace_id,ticket_id,event_index,kind,body) VALUES('workspace','collision',-1,'comment','retained negative index')", []).unwrap();
|
||||
conn.execute_batch("CREATE TABLE host_work(value TEXT); BEGIN IMMEDIATE; INSERT INTO host_work VALUES('pending');").unwrap();
|
||||
assert!(migrate_sqlite_ticket_schema_in_transaction(&conn).is_err());
|
||||
assert!(!conn.is_autocommit());
|
||||
assert_eq!(
|
||||
conn.query_row(
|
||||
"SELECT body FROM typed_tickets WHERE ticket_id='collision'",
|
||||
[],
|
||||
|r| r.get::<_, String>(0)
|
||||
)
|
||||
.unwrap(),
|
||||
"Latest body"
|
||||
);
|
||||
assert_eq!(
|
||||
conn.query_row(
|
||||
"SELECT count(*) FROM pragma_table_info('typed_ticket_events') WHERE name='event_ref'",
|
||||
[],
|
||||
|r| r.get::<_, i64>(0)
|
||||
)
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
conn.query_row(
|
||||
"SELECT max(version) FROM ticket_schema_migrations",
|
||||
[],
|
||||
|r| r.get::<_, i64>(0)
|
||||
)
|
||||
.unwrap(),
|
||||
11
|
||||
);
|
||||
conn.execute_batch("ROLLBACK").unwrap();
|
||||
assert_eq!(
|
||||
conn.query_row("SELECT count(*) FROM host_work", [], |r| r.get::<_, i64>(0))
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
// Remove the deliberately conflicting test row, then retry the unchanged migration.
|
||||
conn.execute(
|
||||
"DELETE FROM typed_ticket_events WHERE event_index=?1",
|
||||
params![-1],
|
||||
)
|
||||
.unwrap();
|
||||
migrate_sqlite_ticket_schema(&conn).unwrap();
|
||||
verify_sqlite_ticket_schema(&conn).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_preserves_published_refs_before_archiving_even_with_sparse_indices() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let path = temp.path().join("identities.db");
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
schema11(&conn);
|
||||
seed_legacy(&conn, "sparse");
|
||||
conn.execute_batch("INSERT INTO typed_ticket_events(workspace_id,ticket_id,event_index,kind,body) VALUES
|
||||
('workspace','sparse',3,'create','Created'),
|
||||
('workspace','sparse',12,'item_edit','Edited'),
|
||||
('workspace','sparse',90,'comment','Existing comment'),
|
||||
('workspace','sparse',99,'comment','Explicit identity');
|
||||
INSERT INTO typed_ticket_event_attributes VALUES('workspace','sparse',99,'event_id','published-custom-id');").unwrap();
|
||||
migrate_sqlite_ticket_schema(&conn).unwrap();
|
||||
let refs: Vec<(i64, String)> = conn.prepare("SELECT event_index,event_ref FROM typed_ticket_events WHERE ticket_id='sparse' ORDER BY event_index").unwrap().query_map([], |r| Ok((r.get(0)?, r.get(1)?))).unwrap().collect::<rusqlite::Result<_>>().unwrap();
|
||||
assert_eq!(
|
||||
refs,
|
||||
vec![
|
||||
(-1, "sparse:-1".into()),
|
||||
(3, "sparse:3".into()),
|
||||
(12, "sparse:12".into()),
|
||||
(90, "sparse:90".into()),
|
||||
(99, "sparse:99".into())
|
||||
]
|
||||
);
|
||||
let backend = SqliteTicketBackend::open_verified(&path, "workspace").unwrap();
|
||||
let ticket = backend.show("sparse".into()).unwrap();
|
||||
assert_eq!(ticket.events[1].event_ref, "sparse:90");
|
||||
assert_eq!(ticket.events[1].attributes["event_id"], "sparse:90");
|
||||
assert_eq!(ticket.events[1].attributes["event_sequence"], "90");
|
||||
let edited = backend
|
||||
.edit_post(
|
||||
"sparse".into(),
|
||||
TicketPostEdit {
|
||||
event_ref: "sparse:90".into(),
|
||||
expected_body: MarkdownText::new("Existing comment"),
|
||||
body: MarkdownText::new("Updated comment"),
|
||||
author: "editor".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(edited.events[1].event_ref, "sparse:90");
|
||||
migrate_sqlite_ticket_schema(&conn).unwrap();
|
||||
assert_eq!(backend.show("sparse".into()).unwrap(), edited);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_preserves_raw_identity_attributes_without_rebinding_published_refs() {
|
||||
for event_ids in [["same", "same"], ["sequence:1", ""], ["collision:-1", ""]] {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let path = temp.path().join("identity-attributes.db");
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
schema11(&conn);
|
||||
seed_legacy(&conn, "collision");
|
||||
for (index, event_id) in event_ids.iter().enumerate() {
|
||||
conn.execute("INSERT INTO typed_ticket_events(workspace_id,ticket_id,event_index,kind,body) VALUES('workspace','collision',?1,'comment','retained')", [index as i64]).unwrap();
|
||||
if !event_id.is_empty() {
|
||||
conn.execute("INSERT INTO typed_ticket_event_attributes VALUES('workspace','collision',?1,'event_id',?2)", params![index as i64,event_id]).unwrap();
|
||||
}
|
||||
}
|
||||
migrate_sqlite_ticket_schema(&conn).unwrap();
|
||||
let backend = SqliteTicketBackend::open_verified(&path, "workspace").unwrap();
|
||||
let migrated = backend.show("collision".into()).unwrap();
|
||||
assert_eq!(
|
||||
migrated
|
||||
.events
|
||||
.iter()
|
||||
.map(|e| e.event_ref.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["collision:-1", "collision:0", "collision:1"]
|
||||
);
|
||||
for (index, event_id) in event_ids.iter().enumerate() {
|
||||
let event = &migrated.events[index + 1];
|
||||
assert_eq!(event.attributes["event_id"], format!("collision:{index}"));
|
||||
if !event_id.is_empty() {
|
||||
assert_eq!(conn.query_row("SELECT value FROM typed_ticket_event_attributes WHERE ticket_id='collision' AND event_index=?1 AND key='event_id'", [index as i64], |r| r.get::<_, String>(0)).unwrap(), *event_id);
|
||||
}
|
||||
}
|
||||
migrate_sqlite_ticket_schema(&conn).unwrap();
|
||||
assert_eq!(backend.show("collision".into()).unwrap(), migrated);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_recovers_only_last_known_body_edit_and_never_invents_creation_author() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let path = temp.path().join("edit-metadata.db");
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
schema11(&conn);
|
||||
for id in [
|
||||
"known",
|
||||
"unknown-last-editor",
|
||||
"title-only",
|
||||
"empty-creator",
|
||||
] {
|
||||
seed_legacy(&conn, id);
|
||||
}
|
||||
conn.execute_batch("INSERT INTO typed_ticket_events(workspace_id,ticket_id,event_index,kind,author,at,body) VALUES
|
||||
('workspace','known',0,'create','creator','original-time','Created'),
|
||||
('workspace','known',1,'item_edit','old-editor','old-time','old body edit'),
|
||||
('workspace','known',5,'item_edit','body-editor','body-time','latest body edit'),
|
||||
('workspace','known',6,'item_edit','title-editor','title-time','later title edit'),
|
||||
('workspace','unknown-last-editor',0,'item_edit','old-editor','old-time','old body edit'),
|
||||
('workspace','unknown-last-editor',1,'item_edit',NULL,NULL,'last body edit'),
|
||||
('workspace','title-only',0,'item_edit','title-editor','title-time','not a body edit'),
|
||||
('workspace','empty-creator',0,'create',' ','create-time','Created');
|
||||
INSERT INTO typed_ticket_event_attributes VALUES
|
||||
('workspace','known',1,'changes','body'),
|
||||
('workspace','known',5,'changes','title,body,targets'),
|
||||
('workspace','known',6,'changes','title'),
|
||||
('workspace','unknown-last-editor',0,'changes','body'),
|
||||
('workspace','unknown-last-editor',1,'changes','body'),
|
||||
('workspace','title-only',0,'changes','title,body_format');
|
||||
INSERT INTO typed_ticket_raw_frontmatter VALUES('workspace','empty-creator','created_by','');").unwrap();
|
||||
migrate_sqlite_ticket_schema(&conn).unwrap();
|
||||
let backend = SqliteTicketBackend::open_verified(&path, "workspace").unwrap();
|
||||
let known = backend.show("known".into()).unwrap();
|
||||
let post = &known.events[0];
|
||||
assert_eq!(post.author.as_deref(), Some("creator"));
|
||||
assert_eq!(post.at.as_deref(), Some("original-time"));
|
||||
assert_eq!(post.edited_at.as_deref(), Some("body-time"));
|
||||
assert_eq!(post.edited_by.as_deref(), Some("body-editor"));
|
||||
assert_eq!(known.document.body.as_str(), "Latest body");
|
||||
for id in ["unknown-last-editor", "title-only", "empty-creator"] {
|
||||
let ticket = backend.show(id.into()).unwrap();
|
||||
assert_eq!(ticket.events[0].edited_at, None, "{id}");
|
||||
assert_eq!(ticket.events[0].edited_by, None, "{id}");
|
||||
assert_eq!(ticket.events[0].author, None, "{id}");
|
||||
assert_eq!(ticket.meta.created_by, None, "{id}");
|
||||
let json = serde_json::to_value(ticket).unwrap();
|
||||
assert!(json["meta"].as_object().unwrap().contains_key("created_by"));
|
||||
assert!(json["meta"]["created_by"].is_null());
|
||||
assert!(json["events"][0]["author"].is_null());
|
||||
}
|
||||
migrate_sqlite_ticket_schema(&conn).unwrap();
|
||||
assert_eq!(backend.show("known".into()).unwrap(), known);
|
||||
// Blank new creation identities use the same honest nullable contract.
|
||||
let mut input = NewTicket::new("Unknown creator");
|
||||
input.author = Some(" ".into());
|
||||
let (_, created) = backend.create_with_snapshot(input).unwrap();
|
||||
assert_eq!(created.meta.created_by, None);
|
||||
assert_eq!(created.events[0].author, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_post_dispatch_round_trips_atomic_ticket_snapshots() {
|
||||
use ticket::{
|
||||
TicketBackendOperation, TicketBackendOperationResult, execute_ticket_backend_operation,
|
||||
};
|
||||
let temp = TempDir::new().unwrap();
|
||||
let backend = SqliteTicketBackend::open(temp.path().join("dispatch.db"), "workspace").unwrap();
|
||||
let (reference, before) = backend
|
||||
.create_with_snapshot(NewTicket::new("Dispatch"))
|
||||
.unwrap();
|
||||
let request = TicketBackendOperation::EditPost {
|
||||
id: reference.id.clone().into(),
|
||||
edit: TicketPostEdit {
|
||||
event_ref: before.events[0].event_ref.clone(),
|
||||
expected_body: before.document.body.clone(),
|
||||
body: MarkdownText::new("Rescoped requirements"),
|
||||
author: "caller".into(),
|
||||
},
|
||||
};
|
||||
let json = serde_json::to_value(&request).unwrap();
|
||||
assert_eq!(json["operation"], "edit_post");
|
||||
let decoded = serde_json::from_value(json).unwrap();
|
||||
let result = execute_ticket_backend_operation(&backend, decoded).unwrap();
|
||||
let wire = serde_json::to_value(&result).unwrap();
|
||||
assert_eq!(wire["result"], "ticket_snapshots");
|
||||
assert_eq!(
|
||||
wire["value"]["previous"]["document"]["body"],
|
||||
before.document.body.as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
wire["value"]["current"]["document"]["body"],
|
||||
"Rescoped requirements"
|
||||
);
|
||||
let TicketBackendOperationResult::TicketSnapshots { previous, current } =
|
||||
serde_json::from_value(wire).unwrap()
|
||||
else {
|
||||
panic!("wrong result variant")
|
||||
};
|
||||
assert_eq!(previous, before);
|
||||
assert_eq!(backend.show(reference.id.into()).unwrap(), current);
|
||||
assert_ne!(
|
||||
ticket_content_digest(&previous),
|
||||
ticket_content_digest(¤t)
|
||||
);
|
||||
assert!(execute_ticket_backend_operation(&backend, request).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user