fix: align merge requests with selector thread contract
This commit is contained in:
@@ -10,6 +10,7 @@ rusqlite.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid = { workspace = true, features = ["v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
+760
-1478
File diff suppressed because it is too large
Load Diff
+186
-378
@@ -1,407 +1,215 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use merge_request::{
|
||||
AssignmentSource, CompleteMergeRequest, ConflictResolution, CurrentAssignment, FindingSeverity,
|
||||
MergeRequestAuth, MergeRequestState, MergeRequestStore, MergeRequestThreadEvent, MergeStrategy,
|
||||
OpenMergeRequest, ReadinessCheck, RegisterReviewCapability, RegisterReviewerChildSession,
|
||||
RepositorySource, RequestForReview, RequestMergeRequestReview, ReviewDecision, ReviewFinding,
|
||||
SubmitMergeRequestReview,
|
||||
};
|
||||
use rusqlite::{Connection, params};
|
||||
|
||||
use merge_request::*;
|
||||
use rusqlite::Connection;
|
||||
use std::sync::{Arc, Mutex};
|
||||
#[derive(Clone)]
|
||||
struct Assignments {
|
||||
current: Arc<Mutex<CurrentAssignment>>,
|
||||
}
|
||||
|
||||
struct Assignments(Arc<Mutex<CurrentAssignment>>);
|
||||
impl AssignmentSource for Assignments {
|
||||
fn current_assignment(
|
||||
&self,
|
||||
_workspace_id: &str,
|
||||
_ticket_id: &str,
|
||||
) -> Result<Option<CurrentAssignment>, String> {
|
||||
Ok(Some(self.current.lock().unwrap().clone()))
|
||||
fn current_assignment(&self, _: &str, _: &str) -> Result<Option<CurrentAssignment>, String> {
|
||||
Ok(Some(self.0.lock().unwrap().clone()))
|
||||
}
|
||||
}
|
||||
|
||||
struct Repositories;
|
||||
|
||||
impl RepositorySource for Repositories {
|
||||
fn repository_belongs_to_workspace(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
repository_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
Ok(workspace_id == "W" && repository_id == "R")
|
||||
}
|
||||
|
||||
fn is_ancestor(
|
||||
&self,
|
||||
_workspace_id: &str,
|
||||
_repository_id: &str,
|
||||
ancestor: &str,
|
||||
descendant: &str,
|
||||
) -> Result<bool, String> {
|
||||
Ok(matches!(
|
||||
(ancestor, descendant),
|
||||
("base", "head-1") | ("base", "head-2")
|
||||
))
|
||||
fn repository_belongs_to_workspace(&self, w: &str, r: &str) -> Result<bool, String> {
|
||||
Ok(w == "W" && r == "R")
|
||||
}
|
||||
}
|
||||
|
||||
fn now(second: u32) -> chrono::DateTime<Utc> {
|
||||
Utc.with_ymd_and_hms(2026, 7, 26, 12, 0, second)
|
||||
fn at(s: u32) -> chrono::DateTime<Utc> {
|
||||
Utc.with_ymd_and_hms(2026, 7, 26, 12, 0, s)
|
||||
.single()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn fixture() -> (tempfile::TempDir, MergeRequestStore, Assignments) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("server.db");
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
conn.execute_batch(
|
||||
"PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE workspaces (workspace_id TEXT PRIMARY KEY);
|
||||
CREATE TABLE repositories (
|
||||
workspace_id TEXT NOT NULL,
|
||||
repository_id TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, repository_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_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,
|
||||
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 NOT NULL, at TEXT NOT NULL,
|
||||
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)
|
||||
);
|
||||
INSERT INTO workspaces VALUES ('W');
|
||||
INSERT INTO repositories VALUES ('W', 'R');
|
||||
INSERT INTO typed_tickets VALUES ('W', 'T', 'inprogress', 1, '2026-07-26T12:00:00Z');",
|
||||
)
|
||||
.unwrap();
|
||||
drop(conn);
|
||||
let assignments = Assignments {
|
||||
current: Arc::new(Mutex::new(CurrentAssignment {
|
||||
assignment_id: "A1".into(),
|
||||
ticket_id: "T".into(),
|
||||
runtime_id: "runtime".into(),
|
||||
worker_id: "coder".into(),
|
||||
})),
|
||||
};
|
||||
let store =
|
||||
MergeRequestStore::open(&path, Arc::new(assignments.clone()), Arc::new(Repositories))
|
||||
.unwrap();
|
||||
(dir, store, assignments)
|
||||
}
|
||||
|
||||
fn auth(assignment_id: &str) -> MergeRequestAuth {
|
||||
fn auth() -> MergeRequestAuth {
|
||||
MergeRequestAuth {
|
||||
workspace_id: "W".into(),
|
||||
repository_id: "R".into(),
|
||||
runtime_id: "runtime".into(),
|
||||
worker_id: "coder".into(),
|
||||
assignment_id: assignment_id.into(),
|
||||
assignment_id: "A".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn open(store: &MergeRequestStore) {
|
||||
store
|
||||
.open_merge_request(OpenMergeRequest {
|
||||
merge_request_id: "MR".into(),
|
||||
ticket_id: "T".into(),
|
||||
repository_id: "R".into(),
|
||||
selector_from: "work/t-feature".into(),
|
||||
selector_to: "develop".into(),
|
||||
request: RequestForReview {
|
||||
base_commit: "base".into(),
|
||||
head_commit: "head-1".into(),
|
||||
changed_paths: vec!["src/lib.rs".into()],
|
||||
summary: "first candidate".into(),
|
||||
},
|
||||
auth: auth("A1"),
|
||||
now: now(1),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn approve(store: &MergeRequestStore, expected_head_commit: &str, token: &str) {
|
||||
store
|
||||
.register_reviewer_child_session(RegisterReviewerChildSession {
|
||||
workspace_id: "W".into(),
|
||||
parent_runtime_id: "runtime".into(),
|
||||
parent_worker_id: "coder".into(),
|
||||
child_session_id: format!("child-{token}"),
|
||||
reviewer_profile: "builtin:reviewer".into(),
|
||||
now: now(2),
|
||||
})
|
||||
.unwrap();
|
||||
store
|
||||
.register_review_capability(RegisterReviewCapability {
|
||||
ticket_id: "T".into(),
|
||||
expected_head_commit: expected_head_commit.into(),
|
||||
child_session_id: format!("child-{token}"),
|
||||
capability_token: token.into(),
|
||||
auth: auth("A1"),
|
||||
now: now(3),
|
||||
})
|
||||
.unwrap();
|
||||
store
|
||||
.submit_review(SubmitMergeRequestReview {
|
||||
ticket_id: "T".into(),
|
||||
expected_head_commit: expected_head_commit.into(),
|
||||
capability_token: token.into(),
|
||||
decision: ReviewDecision::Approve,
|
||||
body: "approved independently".into(),
|
||||
findings: vec![ReviewFinding {
|
||||
severity: FindingSeverity::Note,
|
||||
path: None,
|
||||
line: None,
|
||||
message: "looks good".into(),
|
||||
}],
|
||||
now: now(4),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_drives_review_readiness_and_completion_without_public_revision_identity() {
|
||||
let (_dir, store, _assignments) = fixture();
|
||||
open(&store);
|
||||
approve(&store, "head-1", "token-1");
|
||||
|
||||
let readiness = store
|
||||
.readiness(ReadinessCheck {
|
||||
ticket_id: "T".into(),
|
||||
expected_head_commit: Some("head-1".into()),
|
||||
auth: auth("A1"),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(readiness.ready, "{:?}", readiness.blockers);
|
||||
|
||||
let merged = store
|
||||
.complete(CompleteMergeRequest {
|
||||
ticket_id: "T".into(),
|
||||
expected_head_commit: "head-1".into(),
|
||||
operation_id: "op-1".into(),
|
||||
target_commit: "base".into(),
|
||||
source_commit: "head-1".into(),
|
||||
result_commit: "head-1".into(),
|
||||
strategy: MergeStrategy::FastForward,
|
||||
resolution: ConflictResolution::None,
|
||||
auth: auth("A1"),
|
||||
now: now(5),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(merged.result_commit, "head-1");
|
||||
|
||||
let mr = store.get("W", "T").unwrap();
|
||||
assert_eq!(mr.state, MergeRequestState::Merged);
|
||||
assert!(matches!(
|
||||
mr.thread.last(),
|
||||
Some(MergeRequestThreadEvent::Merge(_))
|
||||
));
|
||||
assert_eq!(mr.selector_from, "work/t-feature");
|
||||
assert_eq!(mr.selector_to, "develop");
|
||||
let json = serde_json::to_string(&mr).unwrap();
|
||||
for forbidden in [
|
||||
"revision_id",
|
||||
"current_revision",
|
||||
"attempt_id",
|
||||
"review_attempt",
|
||||
"head_tree",
|
||||
"diff_digest",
|
||||
"merged_revision_id",
|
||||
] {
|
||||
assert!(
|
||||
!json.contains(forbidden),
|
||||
"unexpected `{forbidden}` in {json}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_review_request_invalidates_prior_approval_and_fences_stale_capability() {
|
||||
let (_dir, store, _assignments) = fixture();
|
||||
open(&store);
|
||||
approve(&store, "head-1", "token-1");
|
||||
store
|
||||
.request_review(RequestMergeRequestReview {
|
||||
ticket_id: "T".into(),
|
||||
expected_head_commit: "head-1".into(),
|
||||
request: RequestForReview {
|
||||
base_commit: "base".into(),
|
||||
head_commit: "head-2".into(),
|
||||
changed_paths: vec!["src/lib.rs".into(), "tests/store.rs".into()],
|
||||
summary: "address review".into(),
|
||||
},
|
||||
auth: auth("A1"),
|
||||
now: now(6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let readiness = store
|
||||
.readiness(ReadinessCheck {
|
||||
ticket_id: "T".into(),
|
||||
expected_head_commit: Some("head-2".into()),
|
||||
auth: auth("A1"),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(!readiness.ready);
|
||||
assert!(
|
||||
readiness
|
||||
.blockers
|
||||
.iter()
|
||||
.any(|value| value.contains("no review result"))
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.submit_review(SubmitMergeRequestReview {
|
||||
ticket_id: "T".into(),
|
||||
expected_head_commit: "head-1".into(),
|
||||
capability_token: "token-1".into(),
|
||||
decision: ReviewDecision::Approve,
|
||||
body: "stale".into(),
|
||||
findings: vec![],
|
||||
now: now(7),
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assignment_change_rejects_candidate_mutation() {
|
||||
let (_dir, store, assignments) = fixture();
|
||||
open(&store);
|
||||
*assignments.current.lock().unwrap() = CurrentAssignment {
|
||||
assignment_id: "A2".into(),
|
||||
fn fixture() -> (tempfile::TempDir, MergeRequestStore) {
|
||||
let d = tempfile::tempdir().unwrap();
|
||||
let p = d.path().join("db");
|
||||
let c = Connection::open(&p).unwrap();
|
||||
c.execute_batch("CREATE TABLE workspaces(workspace_id TEXT PRIMARY KEY);CREATE TABLE repositories(workspace_id TEXT,repository_id TEXT,PRIMARY KEY(workspace_id,repository_id));CREATE TABLE typed_tickets(workspace_id TEXT,ticket_id TEXT,workflow_state TEXT,workflow_state_explicit INTEGER,updated_at TEXT,PRIMARY KEY(workspace_id,ticket_id));CREATE TABLE typed_ticket_events(workspace_id TEXT,ticket_id TEXT,event_index INTEGER,kind TEXT,author TEXT,at 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,ticket_id TEXT,event_index INTEGER,key TEXT,value TEXT,PRIMARY KEY(workspace_id,ticket_id,event_index,key));INSERT INTO workspaces VALUES('W');INSERT INTO repositories VALUES('W','R');INSERT INTO typed_tickets VALUES('W','T','inprogress',1,'t');").unwrap();
|
||||
drop(c);
|
||||
let a = Assignments(Arc::new(Mutex::new(CurrentAssignment {
|
||||
assignment_id: "A".into(),
|
||||
ticket_id: "T".into(),
|
||||
runtime_id: "runtime".into(),
|
||||
worker_id: "other".into(),
|
||||
};
|
||||
let error = store
|
||||
.request_review(RequestMergeRequestReview {
|
||||
worker_id: "coder".into(),
|
||||
})));
|
||||
let s = MergeRequestStore::open(&p, Arc::new(a), Arc::new(Repositories)).unwrap();
|
||||
(d, s)
|
||||
}
|
||||
fn open(s: &MergeRequestStore) {
|
||||
s.open_merge_request(OpenMergeRequest {
|
||||
merge_request_id: "MR".into(),
|
||||
ticket_id: "T".into(),
|
||||
repository_id: "R".into(),
|
||||
selector_from: "work/t".into(),
|
||||
selector_to: "develop".into(),
|
||||
summary: "summary".into(),
|
||||
auth: auth(),
|
||||
now: at(1),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
fn request(s: &MergeRequestStore, subject: &str, token: &str) -> ReviewRequestedEvent {
|
||||
s.register_reviewer_child_session(RegisterReviewerChildSession {
|
||||
workspace_id: "W".into(),
|
||||
parent_runtime_id: "runtime".into(),
|
||||
parent_worker_id: "coder".into(),
|
||||
child_session_id: format!("child-{token}"),
|
||||
reviewer_profile: "builtin:reviewer".into(),
|
||||
now: at(2),
|
||||
})
|
||||
.unwrap();
|
||||
s.request_review(RequestMergeRequestReview {
|
||||
ticket_id: "T".into(),
|
||||
subject_ref: subject.into(),
|
||||
child_session_id: format!("child-{token}"),
|
||||
capability_token: token.into(),
|
||||
auth: auth(),
|
||||
now: at(3),
|
||||
})
|
||||
.unwrap()
|
||||
.request_event
|
||||
}
|
||||
fn approve(s: &MergeRequestStore, subject: &str, token: &str) -> ReviewEvent {
|
||||
request(s, subject, token);
|
||||
s.submit_review(SubmitMergeRequestReview {
|
||||
ticket_id: "T".into(),
|
||||
current_subject_ref: subject.into(),
|
||||
capability_token: token.into(),
|
||||
decision: ReviewDecision::Approve,
|
||||
body: "approved".into(),
|
||||
findings: vec![],
|
||||
now: at(4),
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn selectors_thread_and_completion_have_no_revision_or_commit_api() {
|
||||
let (_d, s) = fixture();
|
||||
open(&s);
|
||||
let review = approve(&s, "opaque-source-ref", "token");
|
||||
let ready = s
|
||||
.readiness(ReadinessCheck {
|
||||
ticket_id: "T".into(),
|
||||
expected_head_commit: "head-1".into(),
|
||||
request: RequestForReview {
|
||||
base_commit: "base".into(),
|
||||
head_commit: "head-2".into(),
|
||||
changed_paths: vec![],
|
||||
summary: String::new(),
|
||||
},
|
||||
auth: auth("A1"),
|
||||
now: now(6),
|
||||
current_subject_ref: Some("opaque-source-ref".into()),
|
||||
auth: auth(),
|
||||
})
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("current assigned worker"));
|
||||
.unwrap();
|
||||
assert!(ready.ready);
|
||||
let merged = s
|
||||
.complete(CompleteMergeRequest {
|
||||
ticket_id: "T".into(),
|
||||
operation_id: "op".into(),
|
||||
approval_event_id: review.event_id,
|
||||
current_subject_ref: "opaque-source-ref".into(),
|
||||
target_ref_before: "old-target-ref".into(),
|
||||
target_ref_after: "new-target-ref".into(),
|
||||
strategy: MergeStrategy::FastForward,
|
||||
resolution: ConflictResolution::None,
|
||||
auth: auth(),
|
||||
now: at(5),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(merged.approved_source_ref, "opaque-source-ref");
|
||||
let mr = s.get("W", "T").unwrap();
|
||||
assert_eq!(mr.selector_from.as_deref(), Some("work/t"));
|
||||
assert_eq!(mr.state, MergeRequestState::Merged);
|
||||
let json = serde_json::to_string(&mr).unwrap();
|
||||
for banned in [
|
||||
"revision_id",
|
||||
"attempt_id",
|
||||
"base_commit",
|
||||
"head_commit",
|
||||
"source_commit",
|
||||
"result_commit",
|
||||
"current_revision",
|
||||
] {
|
||||
assert!(!json.contains(banned), "{banned} in {json}")
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn source_move_cancels_submission_and_old_approval_is_reusable_when_source_returns() {
|
||||
let (_d, s) = fixture();
|
||||
open(&s);
|
||||
let approved = approve(&s, "source-a", "one");
|
||||
request(&s, "source-b", "two");
|
||||
assert!(
|
||||
s.submit_review(SubmitMergeRequestReview {
|
||||
ticket_id: "T".into(),
|
||||
current_subject_ref: "source-c".into(),
|
||||
capability_token: "two".into(),
|
||||
decision: ReviewDecision::Approve,
|
||||
body: "stale".into(),
|
||||
findings: vec![],
|
||||
now: at(6)
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
let mr = s.get("W", "T").unwrap();
|
||||
assert!(
|
||||
mr.thread
|
||||
.iter()
|
||||
.any(|e| matches!(e, MergeRequestThreadEvent::ReviewCancelled(_)))
|
||||
);
|
||||
assert_eq!(
|
||||
mr.effective_review("source-a").map(|r| &r.event_id),
|
||||
Some(&approved.event_id)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn review_revocation_invalidates_readiness() {
|
||||
let (_d, s) = fixture();
|
||||
open(&s);
|
||||
let review = approve(&s, "source", "one");
|
||||
s.revoke_review(RevokeMergeRequestReview {
|
||||
ticket_id: "T".into(),
|
||||
review_event_id: review.event_id,
|
||||
reason: "bad evidence".into(),
|
||||
auth: auth(),
|
||||
now: at(7),
|
||||
})
|
||||
.unwrap();
|
||||
let r = s
|
||||
.readiness(ReadinessCheck {
|
||||
ticket_id: "T".into(),
|
||||
current_subject_ref: Some("source".into()),
|
||||
auth: auth(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(!r.ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v11_migration_builds_thread_events_and_removes_revision_tables() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
conn.execute_batch(
|
||||
"PRAGMA foreign_keys = OFF;
|
||||
CREATE TABLE workspaces (workspace_id TEXT PRIMARY KEY);
|
||||
CREATE TABLE repositories (
|
||||
workspace_id TEXT NOT NULL, repository_id TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, repository_id)
|
||||
);
|
||||
INSERT INTO workspaces VALUES ('W');
|
||||
INSERT INTO repositories VALUES ('W', 'R');
|
||||
CREATE TABLE typed_tickets (
|
||||
workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, ticket_id)
|
||||
);
|
||||
INSERT INTO typed_tickets VALUES ('W', 'T');
|
||||
CREATE TABLE merge_request_schema (singleton INTEGER PRIMARY KEY, version INTEGER NOT NULL);
|
||||
INSERT INTO merge_request_schema VALUES (1, 11);
|
||||
CREATE TABLE merge_requests (
|
||||
workspace_id TEXT, merge_request_id TEXT, ticket_id TEXT, repository_id TEXT,
|
||||
state TEXT, target_ref_selector TEXT, current_revision_id TEXT,
|
||||
opened_by_worker_runtime_id TEXT, opened_by_worker_id TEXT, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE merge_request_revisions (
|
||||
workspace_id TEXT, revision_id TEXT, merge_request_id TEXT, base_commit TEXT,
|
||||
head_commit TEXT, changed_paths_json TEXT, summary TEXT, assignment_id TEXT,
|
||||
coder_worker_runtime_id TEXT, coder_worker_id TEXT, created_at TEXT
|
||||
);
|
||||
CREATE TABLE merge_request_review_attempts (attempt_id TEXT);
|
||||
CREATE TABLE merge_request_reviews (
|
||||
workspace_id TEXT, review_id TEXT, revision_id TEXT,
|
||||
reviewer_worker_runtime_id TEXT, reviewer_worker_id TEXT, reviewer_profile TEXT,
|
||||
decision TEXT, body TEXT, findings_json TEXT, created_at TEXT
|
||||
);
|
||||
CREATE TABLE merge_request_completion_operations (
|
||||
workspace_id TEXT, merge_request_id TEXT, operation_id TEXT, target_commit TEXT,
|
||||
source_commit TEXT, result_commit TEXT, strategy TEXT, resolution TEXT,
|
||||
requested_by_runtime_id TEXT, requested_by_worker_id TEXT, completed_at TEXT, status TEXT
|
||||
);
|
||||
CREATE TABLE merge_request_reviewer_child_sessions (child_session_id TEXT);
|
||||
INSERT INTO merge_requests VALUES (
|
||||
'W', 'MR', 'T', 'R', 'open', 'develop', 'REV',
|
||||
'runtime', 'coder', '2026-07-26T12:00:00Z', '2026-07-26T12:00:00Z'
|
||||
);
|
||||
INSERT INTO merge_request_revisions VALUES (
|
||||
'W', 'REV', 'MR', 'base', 'head-1', '[\"src/lib.rs\"]', 'legacy', 'A1',
|
||||
'runtime', 'coder', '2026-07-26T12:00:00Z'
|
||||
);
|
||||
INSERT INTO merge_request_reviews VALUES (
|
||||
'W', 'REVIEW', 'REV', 'runtime', 'child', 'builtin:reviewer',
|
||||
'approve', 'approved', '[]', '2026-07-26T12:00:01Z'
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
merge_request::migrate(&conn).unwrap();
|
||||
assert_eq!(
|
||||
conn.query_row("SELECT version FROM merge_request_schema", [], |row| row
|
||||
.get::<_, i64>(0))
|
||||
.unwrap(),
|
||||
12
|
||||
);
|
||||
assert_eq!(
|
||||
conn.query_row("SELECT selector_from FROM merge_requests", [], |row| row
|
||||
.get::<_, String>(
|
||||
0
|
||||
))
|
||||
.unwrap(),
|
||||
"head-1"
|
||||
);
|
||||
assert_eq!(
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM merge_request_thread_events",
|
||||
fn v11_migration_preserves_review_events_and_requires_selector_repair() {
|
||||
let c = Connection::open_in_memory().unwrap();
|
||||
c.execute_batch("CREATE TABLE repositories(workspace_id TEXT,repository_id TEXT,PRIMARY KEY(workspace_id,repository_id));CREATE TABLE typed_tickets(workspace_id TEXT,ticket_id TEXT,PRIMARY KEY(workspace_id,ticket_id));INSERT INTO repositories VALUES('W','R');INSERT INTO typed_tickets VALUES('W','T');CREATE TABLE merge_request_schema(singleton INTEGER PRIMARY KEY,version INTEGER);INSERT INTO merge_request_schema VALUES(1,11);CREATE TABLE merge_requests(workspace_id TEXT,merge_request_id TEXT,repository_id TEXT,state TEXT,target_ref_selector TEXT,current_revision_ordinal INTEGER,current_revision_id TEXT,created_at TEXT,updated_at TEXT,merged_revision_id TEXT,merged_at TEXT);CREATE TABLE merge_request_ticket_relations(workspace_id TEXT,merge_request_id TEXT,ticket_id TEXT,relation_kind TEXT,created_at TEXT);CREATE TABLE merge_request_revisions(workspace_id TEXT,merge_request_id TEXT,revision_id TEXT,ordinal INTEGER,base_commit TEXT,head_commit TEXT,diff_digest TEXT,summary TEXT,assignment_id TEXT,created_at TEXT);CREATE TABLE merge_request_revision_paths(workspace_id TEXT,merge_request_id TEXT,revision_id TEXT,ordinal INTEGER,path TEXT);CREATE TABLE merge_request_reviewer_child_sessions(workspace_id TEXT,child_session_id TEXT,parent_runtime_id TEXT,parent_worker_id TEXT,reviewer_profile TEXT,registered_at TEXT);CREATE TABLE merge_request_review_attempts(workspace_id TEXT,attempt_id TEXT,merge_request_id TEXT,ticket_id TEXT,revision_id TEXT,revision_ordinal INTEGER,parent_assignment_id TEXT,parent_runtime_id TEXT,parent_worker_id TEXT,child_session_id TEXT,reviewer_effective_profile TEXT,capability_token TEXT,status TEXT,created_at TEXT,consumed_at TEXT);CREATE TABLE merge_request_reviews(workspace_id TEXT,attempt_id TEXT,merge_request_id TEXT,revision_id TEXT,decision TEXT,body TEXT,submitted_at TEXT);CREATE TABLE merge_request_review_findings(workspace_id TEXT,attempt_id TEXT,ordinal INTEGER,severity TEXT,code TEXT,path TEXT,line INTEGER,body TEXT);CREATE TABLE merge_request_completion_operations(workspace_id TEXT,operation_id TEXT,ticket_id TEXT,revision_id TEXT,authority_kind TEXT,implementation_assignment_id TEXT,completion_actor_runtime_id TEXT,completion_actor_worker_id TEXT,target_commit TEXT,source_commit TEXT,result_commit TEXT,strategy TEXT,resolution TEXT,fingerprint TEXT,status TEXT,result_ticket_state TEXT,created_at TEXT,updated_at TEXT);INSERT INTO merge_requests VALUES('W','MR','R','open','develop',1,'V','2026-07-26T12:00:00Z','2026-07-26T12:00:00Z',NULL,NULL);INSERT INTO merge_request_ticket_relations VALUES('W','MR','T','implements','2026-07-26T12:00:00Z');INSERT INTO merge_request_revisions VALUES('W','MR','V',1,'base','subject','digest','summary','A','2026-07-26T12:00:00Z');INSERT INTO merge_request_review_attempts VALUES('W','AT','MR','T','V',1,'A','runtime','coder','child','builtin:reviewer','token','submitted','2026-07-26T12:00:00Z','2026-07-26T12:00:01Z');INSERT INTO merge_request_reviews VALUES('W','AT','MR','V','approve','approved','2026-07-26T12:00:01Z');").unwrap();
|
||||
merge_request::migrate(&c).unwrap();
|
||||
let selector: Option<String> = c
|
||||
.query_row("SELECT selector_from FROM merge_requests", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert!(selector.is_none());
|
||||
let kinds: String = c
|
||||
.query_row(
|
||||
"SELECT group_concat(kind,',') FROM merge_request_thread_events ORDER BY sequence",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0)
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
for removed in [
|
||||
"merge_request_revisions",
|
||||
"merge_request_review_attempts",
|
||||
"merge_request_reviews",
|
||||
"merge_request_completion_operations",
|
||||
] {
|
||||
let exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1)",
|
||||
params![removed],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!exists, "legacy table `{removed}` still exists");
|
||||
}
|
||||
.unwrap();
|
||||
assert_eq!(kinds, "review_requested,review");
|
||||
let old: bool = c
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE name='merge_request_revisions')",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!old);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user