huskies: merge 864
This commit is contained in:
+60
-1
@@ -21,7 +21,7 @@ pub mod ops;
|
||||
pub mod shadow_write;
|
||||
|
||||
pub use content_store::{all_content_ids, delete_content, read_content, write_content};
|
||||
pub use ops::{delete_item, move_item_stage, next_item_number, write_item_with_content};
|
||||
pub use ops::{ItemMeta, delete_item, move_item_stage, next_item_number, write_item_with_content};
|
||||
pub use shadow_write::init;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -320,6 +320,65 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Story 864: `write_item_with_content` no longer parses YAML front-matter
|
||||
/// from `content`. The CRDT metadata reflects ONLY what the caller passes
|
||||
/// via `ItemMeta`. This test writes a body without any front-matter at
|
||||
/// all, sets metadata explicitly, and asserts the CRDT picks up the typed
|
||||
/// values, not anything derived from `content`.
|
||||
#[test]
|
||||
fn write_item_typed_meta_takes_precedence_over_content() {
|
||||
crate::crdt_state::init_for_test();
|
||||
ensure_content_store();
|
||||
let story_id = "9864_story_typed_meta";
|
||||
|
||||
// Body has NO YAML header — just plain markdown.
|
||||
let content = "# Just a heading\n\nNo front matter here.\n";
|
||||
let meta = ItemMeta {
|
||||
name: Some("Typed Name".into()),
|
||||
agent: Some("coder-1".into()),
|
||||
retry_count: Some(2),
|
||||
blocked: Some(true),
|
||||
depends_on: Some(vec![100, 200]),
|
||||
};
|
||||
write_item_with_content(story_id, "2_current", content, meta);
|
||||
|
||||
let view = crate::crdt_state::read_item(story_id).expect("story exists in CRDT");
|
||||
assert_eq!(view.stage, "2_current");
|
||||
assert_eq!(view.name.as_deref(), Some("Typed Name"));
|
||||
assert_eq!(view.agent.as_deref(), Some("coder-1"));
|
||||
assert_eq!(view.retry_count, Some(2));
|
||||
assert_eq!(view.blocked, Some(true));
|
||||
assert_eq!(view.depends_on, Some(vec![100, 200]));
|
||||
|
||||
// Content is stored verbatim (no parsing, no rewrite).
|
||||
assert_eq!(read_content(story_id).as_deref(), Some(content));
|
||||
}
|
||||
|
||||
/// Story 864: passing `ItemMeta::default()` against a content blob that
|
||||
/// LOOKS like front-matter must NOT silently extract metadata into the
|
||||
/// CRDT. The whole point of removing the implicit YAML round-trip is
|
||||
/// that metadata only flows in through the typed `ItemMeta` arg.
|
||||
#[test]
|
||||
fn write_item_default_meta_ignores_yaml_in_content() {
|
||||
crate::crdt_state::init_for_test();
|
||||
ensure_content_store();
|
||||
let story_id = "9864_story_yaml_ignored";
|
||||
|
||||
let content = "---\nname: Should Not Appear\nagent: ghost\n---\n# Body\n";
|
||||
write_item_with_content(story_id, "2_current", content, ItemMeta::default());
|
||||
|
||||
let view = crate::crdt_state::read_item(story_id).expect("story exists in CRDT");
|
||||
assert_eq!(view.stage, "2_current");
|
||||
assert_eq!(
|
||||
view.name, None,
|
||||
"name must come from typed meta, not parsed YAML"
|
||||
);
|
||||
assert_eq!(
|
||||
view.agent, None,
|
||||
"agent must come from typed meta, not parsed YAML"
|
||||
);
|
||||
}
|
||||
|
||||
/// Bug 780: stage transitions must reset retry_count to 0 in the CRDT.
|
||||
/// Carryover from prior-stage retries was tripping the auto-assigner's
|
||||
/// deterministic-merge skip logic.
|
||||
|
||||
+65
-23
@@ -9,23 +9,65 @@ use super::content_store::{
|
||||
use super::shadow_write::{PIPELINE_DB, PipelineWriteMsg};
|
||||
use crate::io::story_metadata::parse_front_matter;
|
||||
|
||||
/// Typed metadata for a pipeline item write.
|
||||
///
|
||||
/// Replaces the prior YAML-parsing write path (story 864): callers now pass
|
||||
/// metadata explicitly instead of round-tripping it through a serialized
|
||||
/// front-matter blob. Every field is `Option`-typed; `None` means
|
||||
/// "leave unchanged" on update, "use the default" on insert.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ItemMeta {
|
||||
pub name: Option<String>,
|
||||
pub agent: Option<String>,
|
||||
pub retry_count: Option<i64>,
|
||||
pub blocked: Option<bool>,
|
||||
pub depends_on: Option<Vec<u32>>,
|
||||
}
|
||||
|
||||
impl ItemMeta {
|
||||
/// Convenience constructor for the common "just set a name" case.
|
||||
#[cfg(test)]
|
||||
pub fn named(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: Some(name.into()),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse YAML front-matter from a content string into typed metadata.
|
||||
///
|
||||
/// This is an explicit caller-side conversion — the write path itself
|
||||
/// no longer parses YAML. Use this when the caller has a raw content
|
||||
/// string with front-matter and wants the metadata to flow into the
|
||||
/// CRDT. Returns `Self::default()` if parsing fails or there is no
|
||||
/// front-matter present.
|
||||
pub fn from_yaml(content: &str) -> Self {
|
||||
match parse_front_matter(content) {
|
||||
Ok(m) => Self {
|
||||
name: m.name,
|
||||
agent: m.agent,
|
||||
retry_count: m.retry_count.map(|r| r as i64),
|
||||
blocked: m.blocked,
|
||||
depends_on: m.depends_on,
|
||||
},
|
||||
Err(_) => Self::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a pipeline item from in-memory content (no filesystem access).
|
||||
///
|
||||
/// This is the primary write path for the DB-backed pipeline. It updates
|
||||
/// the CRDT, the in-memory content store, and the SQLite shadow table.
|
||||
pub fn write_item_with_content(story_id: &str, stage: &str, content: &str) {
|
||||
let (name, agent, retry_count, blocked, depends_on) = match parse_front_matter(content) {
|
||||
Ok(meta) => (
|
||||
meta.name,
|
||||
meta.agent,
|
||||
meta.retry_count.map(|r| r as i64),
|
||||
meta.blocked,
|
||||
meta.depends_on
|
||||
.as_ref()
|
||||
.and_then(|d| serde_json::to_string(d).ok()),
|
||||
),
|
||||
Err(_) => (None, None, None, None, None),
|
||||
};
|
||||
///
|
||||
/// The metadata in `meta` is authoritative: this function does NOT parse
|
||||
/// `content` to extract front-matter fields. Callers must pass typed
|
||||
/// metadata explicitly via `ItemMeta`.
|
||||
pub fn write_item_with_content(story_id: &str, stage: &str, content: &str, meta: ItemMeta) {
|
||||
let depends_on_json = meta
|
||||
.depends_on
|
||||
.as_ref()
|
||||
.and_then(|d| serde_json::to_string(d).ok());
|
||||
|
||||
// Update in-memory content store.
|
||||
ensure_content_store();
|
||||
@@ -42,11 +84,11 @@ pub fn write_item_with_content(story_id: &str, stage: &str, content: &str) {
|
||||
crate::crdt_state::write_item(
|
||||
story_id,
|
||||
stage,
|
||||
name.as_deref(),
|
||||
agent.as_deref(),
|
||||
retry_count,
|
||||
blocked,
|
||||
depends_on.as_deref(),
|
||||
meta.name.as_deref(),
|
||||
meta.agent.as_deref(),
|
||||
meta.retry_count,
|
||||
meta.blocked,
|
||||
depends_on_json.as_deref(),
|
||||
None,
|
||||
None,
|
||||
merged_at_ts,
|
||||
@@ -57,11 +99,11 @@ pub fn write_item_with_content(story_id: &str, stage: &str, content: &str) {
|
||||
let msg = PipelineWriteMsg {
|
||||
story_id: story_id.to_string(),
|
||||
stage: stage.to_string(),
|
||||
name,
|
||||
agent,
|
||||
retry_count,
|
||||
blocked,
|
||||
depends_on,
|
||||
name: meta.name,
|
||||
agent: meta.agent,
|
||||
retry_count: meta.retry_count,
|
||||
blocked: meta.blocked,
|
||||
depends_on: depends_on_json,
|
||||
content: Some(content.to_string()),
|
||||
};
|
||||
let _ = db.tx.send(msg);
|
||||
|
||||
Reference in New Issue
Block a user