Files
yoi/crates/merge-request/tests/store.rs
T

299 lines
11 KiB
Rust

use merge_request::*;
use rusqlite::{Connection, params};
use std::sync::{Arc, Barrier};
use std::thread;
use tempfile::TempDir;
fn setup() -> (TempDir, SqliteMergeRequestStore) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("server.db");
let conn = Connection::open(&path).unwrap();
conn.execute_batch(r#"
PRAGMA foreign_keys=ON;
CREATE TABLE repositories(workspace_id TEXT NOT NULL,repository_id TEXT NOT NULL,PRIMARY KEY(workspace_id,repository_id));
CREATE TABLE typed_tickets(workspace_id TEXT NOT NULL,ticket_id TEXT NOT NULL,workflow_state TEXT NOT NULL,workflow_state_explicit INTEGER NOT NULL DEFAULT 1,updated_at TEXT NOT NULL,PRIMARY KEY(workspace_id,ticket_id));
CREATE TABLE typed_ticket_events(workspace_id TEXT NOT NULL,ticket_id TEXT NOT NULL,event_index INTEGER NOT NULL,kind TEXT NOT NULL,author TEXT,at TEXT,status TEXT,from_state TEXT,to_state TEXT,heading TEXT,body TEXT,PRIMARY KEY(workspace_id,ticket_id,event_index));
CREATE TABLE typed_ticket_event_attributes(workspace_id TEXT NOT NULL,ticket_id TEXT NOT NULL,event_index INTEGER NOT NULL,key TEXT NOT NULL,value TEXT NOT NULL,PRIMARY KEY(workspace_id,ticket_id,event_index,key));
CREATE TABLE ticket_worker_assignments(workspace_id TEXT NOT NULL,ticket_id TEXT NOT NULL,assignment_id TEXT NOT NULL,runtime_id TEXT NOT NULL,worker_id TEXT NOT NULL,PRIMARY KEY(workspace_id,ticket_id,assignment_id));
CREATE TABLE ticket_current_worker_assignments(workspace_id TEXT NOT NULL,ticket_id TEXT NOT NULL,assignment_id TEXT NOT NULL,runtime_id TEXT NOT NULL,worker_id TEXT NOT NULL,PRIMARY KEY(workspace_id,ticket_id));
"#).unwrap();
for ws in ["ws-a", "ws-b"] {
conn.execute("INSERT INTO repositories VALUES(?1,'repo')", params![ws])
.unwrap();
conn.execute(
"INSERT INTO typed_tickets VALUES(?1,'T1','inprogress',1,'t0')",
params![ws],
)
.unwrap();
conn.execute(
"INSERT INTO ticket_worker_assignments VALUES(?1,'T1','A1','R1','W1')",
params![ws],
)
.unwrap();
conn.execute(
"INSERT INTO ticket_current_worker_assignments VALUES(?1,'T1','A1','R1','W1')",
params![ws],
)
.unwrap();
}
drop(conn);
let store = SqliteMergeRequestStore::open(&path, "ws-a").unwrap();
(dir, store)
}
fn revision(id: &str, ordinal: u64, head: &str) -> MergeRequestRevision {
MergeRequestRevision {
revision_id: id.into(),
ordinal,
base_commit: "base".into(),
head_commit: head.into(),
diff_digest: format!("sha256:diff-{head}"),
changed_paths: vec!["src/lib.rs".into()],
summary: format!("revision {id}"),
assignment_id: "A1".into(),
created_at: format!("t{ordinal}"),
}
}
fn open(store: &SqliteMergeRequestStore) {
store
.open_merge_request(OpenMergeRequest {
merge_request_id: "MR1".into(),
ticket_id: "T1".into(),
repository_id: "repo".into(),
target_ref_selector: "refs/heads/develop".into(),
revision: revision("V1", 1, "head"),
authenticated_runtime_id: "R1".into(),
authenticated_worker_id: "W1".into(),
now: "t1".into(),
})
.unwrap();
}
fn attempt(store: &SqliteMergeRequestStore, revision: &str, token: &str) {
let child = format!("child-{revision}");
store
.register_reviewer_child_session(RegisterReviewerChildSession {
parent_runtime_id: "R1".into(),
parent_worker_id: "W1".into(),
child_session_id: child.clone(),
now: "t2".into(),
})
.unwrap();
store
.register_review_attempt(RegisterReviewAttempt {
attempt_id: format!("attempt-{revision}"),
ticket_id: "T1".into(),
revision_id: revision.into(),
parent_assignment_id: "A1".into(),
parent_runtime_id: "R1".into(),
parent_worker_id: "W1".into(),
child_session_id: child,
capability_token: token.into(),
now: "t2".into(),
})
.unwrap();
}
fn approve(store: &SqliteMergeRequestStore, revision: &str, token: &str) {
attempt(store, revision, token);
store
.submit_review(SubmitReview {
ticket_id: "T1".into(),
revision_id: revision.into(),
capability_token: token.into(),
decision: ReviewDecision::Approve,
body: "approved".into(),
findings: vec![],
now: "t3".into(),
})
.unwrap();
}
fn completion(operation_id: &str) -> CompleteMergeRequest {
CompleteMergeRequest {
operation_id: operation_id.into(),
ticket_id: "T1".into(),
expected_revision_id: "V1".into(),
target_commit: "base".into(),
source_commit: "head".into(),
result_commit: "head".into(),
strategy: MergeStrategy::FastForward,
resolution: MergeResolution::None,
implementation_assignment_id: "A1".into(),
completion_actor_runtime_id: "OR".into(),
completion_actor_worker_id: "OW".into(),
now: "t4".into(),
}
}
#[test]
fn target_movement_does_not_invalidate_source_revision_approval() {
let (_dir, store) = setup();
open(&store);
approve(&store, "V1", "token-v1");
for target in ["base", "advanced-target"] {
let readiness = store
.readiness_for_ticket_with_target("T1", Some(target))
.unwrap();
assert!(
readiness.ready,
"target movement must not invalidate source approval"
);
assert_eq!(readiness.review_status, ReviewStatus::Approved);
assert_eq!(readiness.observed_target_commit.as_deref(), Some(target));
}
store
.add_revision(AddRevision {
ticket_id: "T1".into(),
expected_current_revision_id: "V1".into(),
revision: revision("V2", 2, "head2"),
authenticated_runtime_id: "R1".into(),
authenticated_worker_id: "W1".into(),
now: "t5".into(),
})
.unwrap();
assert_eq!(
store.readiness_for_ticket("T1").unwrap().review_status,
ReviewStatus::Pending
);
}
#[test]
fn completion_records_one_final_merge_outcome_and_replays_idempotently() {
let (_dir, store) = setup();
open(&store);
approve(&store, "V1", "token-v1");
let first = store.complete(completion("OP1")).unwrap();
assert!(!first.replayed);
assert_eq!(first.ticket_state, "done");
let merged = store.show_for_ticket("T1").unwrap().unwrap();
assert_eq!(merged.state, MergeRequestState::Merged);
assert_eq!(merged.merged_revision_id.as_deref(), Some("V1"));
assert_eq!(merged.merged_target_commit.as_deref(), Some("base"));
assert_eq!(merged.merged_result_commit.as_deref(), Some("head"));
assert_eq!(merged.merge_strategy, Some(MergeStrategy::FastForward));
assert_eq!(merged.merge_resolution, Some(MergeResolution::None));
assert_eq!(merged.merged_by_runtime_id.as_deref(), Some("OR"));
assert_eq!(merged.merged_by_worker_id.as_deref(), Some("OW"));
assert!(store.complete(completion("OP1")).unwrap().replayed);
let mut conflicting = completion("OP1");
conflicting.target_commit = "other".into();
assert!(matches!(
store.complete(conflicting),
Err(MergeRequestError::OperationConflict)
));
}
#[test]
fn completion_rejects_invalid_or_non_current_source_outcomes_without_side_effects() {
let (_dir, store) = setup();
open(&store);
approve(&store, "V1", "token-v1");
let mut invalid_ff = completion("bad-ff");
invalid_ff.result_commit = "different".into();
assert!(matches!(
store.complete(invalid_ff),
Err(MergeRequestError::InvalidMergeOutcome(_))
));
let mut invalid_merge = completion("bad-merge");
invalid_merge.strategy = MergeStrategy::Merge;
assert!(matches!(
store.complete(invalid_merge),
Err(MergeRequestError::InvalidMergeOutcome(_))
));
let mut wrong_source = completion("wrong-source");
wrong_source.source_commit = "not-approved".into();
wrong_source.result_commit = "not-approved".into();
assert!(matches!(
store.complete(wrong_source),
Err(MergeRequestError::InvalidMergeOutcome(_))
));
assert_eq!(
store.show_for_ticket("T1").unwrap().unwrap().state,
MergeRequestState::Open
);
let conn = Connection::open(store.db_path()).unwrap();
assert_eq!(
conn.query_row(
"SELECT workflow_state FROM typed_tickets WHERE workspace_id='ws-a' AND ticket_id='T1'",
[],
|row| row.get::<_, String>(0)
)
.unwrap(),
"inprogress"
);
}
#[test]
fn concurrent_completion_converges_on_one_operation() {
let (_dir, store) = setup();
open(&store);
approve(&store, "V1", "token-v1");
let path = store.db_path().to_path_buf();
let barrier = Arc::new(Barrier::new(3));
let mut handles = Vec::new();
for _ in 0..2 {
let path = path.clone();
let barrier = barrier.clone();
handles.push(thread::spawn(move || {
let store = SqliteMergeRequestStore::open_verified(path, "ws-a").unwrap();
barrier.wait();
store.complete(completion("OP-concurrent"))
}));
}
barrier.wait();
let outcomes: Vec<_> = handles
.into_iter()
.map(|handle| handle.join().unwrap().unwrap())
.collect();
assert_eq!(
outcomes.iter().filter(|outcome| !outcome.replayed).count(),
1
);
assert_eq!(
outcomes.iter().filter(|outcome| outcome.replayed).count(),
1
);
}
#[test]
fn reviewer_attempt_is_bound_to_direct_child_and_current_assignment() {
let (_dir, store) = setup();
open(&store);
store
.register_reviewer_child_session(RegisterReviewerChildSession {
parent_runtime_id: "R1".into(),
parent_worker_id: "W1".into(),
child_session_id: "child".into(),
now: "t2".into(),
})
.unwrap();
store
.register_review_attempt(RegisterReviewAttempt {
attempt_id: "attempt".into(),
ticket_id: "T1".into(),
revision_id: "V1".into(),
parent_assignment_id: "A1".into(),
parent_runtime_id: "R1".into(),
parent_worker_id: "W1".into(),
child_session_id: "child".into(),
capability_token: "token".into(),
now: "t2".into(),
})
.unwrap();
let wrong_token = store.submit_review(SubmitReview {
ticket_id: "T1".into(),
revision_id: "V1".into(),
capability_token: "wrong".into(),
decision: ReviewDecision::Approve,
body: "approved".into(),
findings: vec![],
now: "t3".into(),
});
assert!(matches!(
wrong_token,
Err(MergeRequestError::InvalidReviewAttempt)
));
}