huskies: merge 869
This commit is contained in:
@@ -52,7 +52,8 @@ pub use types::{
|
||||
TestJobCrdt, TestJobView, TokenUsageCrdt, TokenUsageView, subscribe,
|
||||
};
|
||||
pub use write::{
|
||||
migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id, write_item,
|
||||
migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id, set_qa_mode,
|
||||
write_item,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -315,6 +315,11 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let qa_mode = match item.qa_mode.view() {
|
||||
JsonValue::String(s) if !s.is_empty() => Some(s),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Some(PipelineItemView {
|
||||
story_id,
|
||||
stage,
|
||||
@@ -326,6 +331,7 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
|
||||
claimed_by,
|
||||
claimed_at,
|
||||
merged_at,
|
||||
qa_mode,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,9 @@ pub struct PipelineItemCrdt {
|
||||
/// Written once when the item transitions to `5_done`. Used by the
|
||||
/// sweep loop to determine when to promote to `6_archived`.
|
||||
pub merged_at: LwwRegisterCrdt<f64>,
|
||||
/// QA mode override for this item: `"server"`, `"agent"`, or `"human"`.
|
||||
/// Empty string means "use the project default".
|
||||
pub qa_mode: LwwRegisterCrdt<String>,
|
||||
}
|
||||
|
||||
/// CRDT node that holds a single peer's presence entry.
|
||||
@@ -122,6 +125,9 @@ pub struct PipelineItemView {
|
||||
/// Unix timestamp (seconds) when the item was merged to master.
|
||||
/// `None` for items that were never in `5_done` or for legacy items.
|
||||
pub merged_at: Option<f64>,
|
||||
/// QA mode override from the CRDT register: `"server"`, `"agent"`, or `"human"`.
|
||||
/// `None` means the register is unset (use project default).
|
||||
pub qa_mode: Option<String>,
|
||||
}
|
||||
|
||||
/// A snapshot of a single node presence entry derived from the CRDT document.
|
||||
|
||||
@@ -8,6 +8,7 @@ use serde_json::json;
|
||||
|
||||
use super::state::{apply_and_persist, emit_event, get_crdt, rebuild_index};
|
||||
use super::types::{CrdtEvent, PipelineDoc, PipelineItemCrdt};
|
||||
use crate::io::story_metadata::QaMode;
|
||||
use crate::slog;
|
||||
|
||||
// ── Name migration helpers ────────────────────────────────────────────
|
||||
@@ -185,6 +186,29 @@ pub fn migrate_names_from_slugs() {
|
||||
slog!("[crdt] Migrated names for {count} items from story ID slugs");
|
||||
}
|
||||
|
||||
/// Set the typed `qa_mode` CRDT register for a pipeline item.
|
||||
///
|
||||
/// Passing `Some(mode)` writes the mode string (e.g. `"server"`, `"agent"`, `"human"`)
|
||||
/// to the item's `qa_mode` register and persists a signed op.
|
||||
/// Passing `None` clears the register to an empty string, which means
|
||||
/// "use the project default" (same as if the field was never set).
|
||||
///
|
||||
/// Returns `true` if the item was found and the op was applied, `false` otherwise.
|
||||
pub fn set_qa_mode(story_id: &str, mode: Option<QaMode>) -> bool {
|
||||
let Some(state_mutex) = get_crdt() else {
|
||||
return false;
|
||||
};
|
||||
let Ok(mut state) = state_mutex.lock() else {
|
||||
return false;
|
||||
};
|
||||
let Some(&idx) = state.index.get(story_id) else {
|
||||
return false;
|
||||
};
|
||||
let value = mode.map(|m| m.as_str().to_string()).unwrap_or_default();
|
||||
apply_and_persist(&mut state, |s| s.crdt.doc.items[idx].qa_mode.set(value));
|
||||
true
|
||||
}
|
||||
|
||||
/// Write a pipeline item state through CRDT operations.
|
||||
///
|
||||
/// If the item exists, updates its registers. If not, inserts a new item
|
||||
@@ -287,6 +311,7 @@ pub fn write_item(
|
||||
"claimed_by": claimed_by.unwrap_or(""),
|
||||
"claimed_at": claimed_at.unwrap_or(0.0),
|
||||
"merged_at": merged_at.unwrap_or(0.0),
|
||||
"qa_mode": "",
|
||||
})
|
||||
.into();
|
||||
|
||||
@@ -312,6 +337,7 @@ pub fn write_item(
|
||||
item.claimed_by.advance_seq(floor);
|
||||
item.claimed_at.advance_seq(floor);
|
||||
item.merged_at.advance_seq(floor);
|
||||
item.qa_mode.advance_seq(floor);
|
||||
}
|
||||
|
||||
// Broadcast a CrdtEvent for the new item.
|
||||
@@ -613,6 +639,64 @@ mod tests {
|
||||
// We call it here just to confirm no panic.
|
||||
migrate_names_from_slugs();
|
||||
}
|
||||
|
||||
// ── set_qa_mode regression tests ─────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn set_qa_mode_round_trip_server_then_human() {
|
||||
use crate::io::story_metadata::QaMode;
|
||||
init_for_test();
|
||||
|
||||
write_item(
|
||||
"869_story_qa_roundtrip",
|
||||
"1_backlog",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Set qa=server via typed path and assert CRDT register reflects it.
|
||||
let ok = set_qa_mode("869_story_qa_roundtrip", Some(QaMode::Server));
|
||||
assert!(ok, "set_qa_mode should return true for known item");
|
||||
let view = read_item("869_story_qa_roundtrip").unwrap();
|
||||
assert_eq!(
|
||||
view.qa_mode.as_deref(),
|
||||
Some("server"),
|
||||
"CRDT register should hold \"server\""
|
||||
);
|
||||
|
||||
// Set qa=human via typed path and assert CRDT register is updated.
|
||||
let ok = set_qa_mode("869_story_qa_roundtrip", Some(QaMode::Human));
|
||||
assert!(ok, "set_qa_mode should return true for known item");
|
||||
let view = read_item("869_story_qa_roundtrip").unwrap();
|
||||
assert_eq!(
|
||||
view.qa_mode.as_deref(),
|
||||
Some("human"),
|
||||
"CRDT register should hold \"human\""
|
||||
);
|
||||
|
||||
// Clear via None — register goes back to unset.
|
||||
let ok = set_qa_mode("869_story_qa_roundtrip", None);
|
||||
assert!(ok, "set_qa_mode(None) should return true");
|
||||
let view = read_item("869_story_qa_roundtrip").unwrap();
|
||||
assert_eq!(
|
||||
view.qa_mode, None,
|
||||
"clearing qa_mode should leave register unset"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_qa_mode_returns_false_for_unknown_story() {
|
||||
init_for_test();
|
||||
use crate::io::story_metadata::QaMode;
|
||||
let ok = set_qa_mode("nonexistent_story_qa", Some(QaMode::Server));
|
||||
assert!(!ok, "set_qa_mode should return false for unknown story_id");
|
||||
}
|
||||
use bft_json_crdt::json_crdt::OpState;
|
||||
use bft_json_crdt::keypair::make_keypair;
|
||||
use bft_json_crdt::op::ROOT_ID;
|
||||
|
||||
Reference in New Issue
Block a user