use anyhow::{Result, bail};
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
use std::collections::HashMap;
pub const BACKLOG_COMPONENT: &str = "backlog";
pub const BACKLOG_ALIAS: &str = "pending";
pub const BACKLOG_DONE_COMPONENT: &str = "done";
pub const REVIEW_COMPONENT: &str = "review";
pub const ICEBOX_COMPONENT: &str = "icebox";
const EXCHANGE_COMPONENT: &str = "exchange";
pub fn is_backlog_component(name: &str) -> bool {
name == BACKLOG_COMPONENT || name == BACKLOG_ALIAS
}
pub fn is_backlog_done_component(name: &str) -> bool {
name == BACKLOG_DONE_COMPONENT
}
pub fn is_review_component(name: &str) -> bool {
name == REVIEW_COMPONENT
}
pub fn is_icebox_component(name: &str) -> bool {
name == ICEBOX_COMPONENT
}
pub fn is_tracked_work_component(name: &str) -> bool {
is_backlog_component(name) || is_review_component(name) || is_icebox_component(name)
}
pub fn strip_backlog_patch_attr(doc: &str) -> String {
let mut result = doc.to_string();
let mut warned = false;
for name in &[BACKLOG_COMPONENT, BACKLOG_ALIAS] {
let prefix = format!("<!-- agent:{} ", name);
while let Some(start) = result.find(&prefix) {
let tag_start = start;
let rest = &result[start + prefix.len()..];
let Some(end_rel) = rest.find("-->") else {
break;
};
let attrs_text = &rest[..end_rel];
let tag_end = start + prefix.len() + end_rel + 3;
let tokens: Vec<&str> = attrs_text.split_whitespace().collect();
let has_patch_or_mode = tokens.iter().any(|t| {
t.split_once('=')
.map(|(k, _)| k == "patch" || k == "mode")
.unwrap_or(false)
});
if !has_patch_or_mode {
break;
}
if !warned {
eprintln!(
"[deprecation] patch/mode attribute on agent:{} is deprecated and will be stripped — \
the binary owns backlog mutations via --pending-* flags",
name
);
warned = true;
}
let remaining: Vec<&str> = tokens
.iter()
.filter(|t| {
t.split_once('=')
.map(|(k, _)| k != "patch" && k != "mode")
.unwrap_or(true)
})
.copied()
.collect();
let new_tag = if remaining.is_empty() {
format!("<!-- agent:{} -->", name)
} else {
format!("<!-- agent:{} {} -->", name, remaining.join(" "))
};
result.replace_range(tag_start..tag_end, &new_tag);
}
}
result
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Component {
pub name: String,
pub attrs: HashMap<String, String>,
pub open_start: usize,
pub open_end: usize,
pub close_start: usize,
pub close_end: usize,
}
impl Component {
#[allow(dead_code)] pub fn content<'a>(&self, doc: &'a str) -> &'a str {
&doc[self.open_end..self.close_start]
}
pub fn patch_mode(&self) -> Option<&str> {
self.attrs
.get("patch")
.map(|s| s.as_str())
.or_else(|| self.attrs.get("mode").map(|s| s.as_str()))
}
pub fn replace_content(&self, doc: &str, new_content: &str) -> String {
let mut result = String::with_capacity(doc.len() + new_content.len());
result.push_str(&doc[..self.open_end]);
result.push_str(new_content);
result.push_str(&doc[self.close_start..]);
result
}
pub fn append_with_caret(
&self,
doc: &str,
content: &str,
caret_offset: Option<usize>,
) -> String {
let existing = &doc[self.open_end..self.close_start];
if append_patch_already_present(existing, content) {
return doc.to_string();
}
if let Some(caret) = caret_offset {
if caret > self.open_end && caret <= self.close_start {
let insert_at = doc[..caret]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(self.open_end);
let insert_at = insert_at.max(self.open_end);
let prior = &doc[self.open_end..insert_at];
let mut result = String::with_capacity(doc.len() + content.len() + 1);
result.push_str(&doc[..insert_at]);
if needs_exchange_heading_separator(&self.name, prior, content) {
result.push('\n');
}
result.push_str(content.trim_end());
result.push('\n');
result.push_str(&doc[insert_at..]);
return result;
}
}
let mut result = String::with_capacity(doc.len() + content.len() + 1);
result.push_str(&doc[..self.open_end]);
let trimmed_existing = existing.trim_end();
result.push_str(trimmed_existing);
if needs_exchange_heading_separator(&self.name, trimmed_existing, content) {
result.push('\n');
}
result.push('\n');
result.push_str(content.trim_end());
result.push('\n');
result.push_str(&doc[self.close_start..]);
result
}
pub fn append_with_boundary(&self, doc: &str, content: &str, boundary_id: &str) -> String {
let boundary_marker = format!("<!-- agent:boundary:{} -->", boundary_id);
let content_region = &doc[self.open_end..self.close_start];
if append_patch_already_present(content_region, content) {
return doc.to_string();
}
let code_ranges = find_code_ranges(doc);
let mut search_from = 0;
let found_pos = loop {
match content_region[search_from..].find(&boundary_marker) {
Some(rel_pos) => {
let abs_pos = self.open_end + search_from + rel_pos;
if code_ranges
.iter()
.any(|&(cs, ce)| abs_pos >= cs && abs_pos < ce)
{
search_from += rel_pos + boundary_marker.len();
continue;
}
break Some(abs_pos);
}
None => break None,
}
};
if let Some(abs_pos) = found_pos {
let line_start = doc[..abs_pos]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(self.open_end)
.max(self.open_end);
let marker_end = abs_pos + boundary_marker.len();
let line_end = if marker_end < self.close_start
&& doc.as_bytes().get(marker_end) == Some(&b'\n')
{
marker_end + 1
} else {
marker_end
};
let line_end = line_end.min(self.close_start);
let new_id = crate::id::new_boundary_id();
let new_marker = crate::id::format_boundary_marker(&new_id);
let mut result = String::with_capacity(doc.len() + content.len() + new_marker.len());
let prior = &doc[self.open_end..line_start];
result.push_str(&doc[..line_start]);
if needs_exchange_heading_separator(&self.name, prior, content) {
result.push('\n');
}
result.push_str(content.trim_end());
result.push('\n');
result.push_str(&new_marker);
result.push('\n');
result.push_str(&doc[line_end..]);
return result;
}
self.append_with_caret(doc, content, None)
}
}
fn is_valid_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
let first = name.as_bytes()[0];
if !first.is_ascii_alphanumeric() {
return false;
}
name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
}
fn append_patch_already_present(existing: &str, content: &str) -> bool {
let patch = normalize_append_patch_content(content);
if patch.is_empty() {
return false;
}
normalize_append_patch_content(existing).contains(&patch)
}
fn needs_exchange_heading_separator(component_name: &str, prior: &str, content: &str) -> bool {
component_name == EXCHANGE_COMPONENT
&& !prior.trim().is_empty()
&& !prior.ends_with("\n\n")
&& starts_with_response_heading(content)
}
fn starts_with_response_heading(content: &str) -> bool {
content.trim_start().starts_with("### Re:")
}
fn normalize_append_patch_content(content: &str) -> String {
content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if trimmed.starts_with("<!-- agent:boundary:") && trimmed.ends_with(" -->") {
None
} else if markdown_heading_has_head_marker(line) {
Some(line.trim_end_matches(" (HEAD)").to_string())
} else {
Some(line.to_string())
}
})
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string()
}
fn markdown_heading_has_head_marker(line: &str) -> bool {
let trimmed = line.trim_start();
let hashes = trimmed.bytes().take_while(|b| *b == b'#').count();
(1..=6).contains(&hashes)
&& trimmed.as_bytes().get(hashes) == Some(&b' ')
&& line.ends_with(" (HEAD)")
}
pub fn is_agent_marker(comment_text: &str) -> bool {
let trimmed = comment_text.trim();
if let Some(rest) = trimmed.strip_prefix("/agent:") {
is_valid_name(rest)
} else if let Some(rest) = trimmed.strip_prefix("agent:") {
let name_part = rest.split_whitespace().next().unwrap_or("");
is_valid_name(name_part)
} else {
false
}
}
fn bounded_preview(doc: &str, start: usize, max_bytes: usize) -> &str {
debug_assert!(doc.is_char_boundary(start));
let mut end = doc.len().min(start.saturating_add(max_bytes));
while end > start && !doc.is_char_boundary(end) {
end -= 1;
}
&doc[start..end]
}
pub fn parse_attrs(attr_text: &str) -> HashMap<String, String> {
let mut attrs = HashMap::new();
for token in attr_text.split_whitespace() {
if let Some((key, value)) = token.split_once('=') {
if !key.is_empty() && !value.is_empty() {
attrs.insert(key.to_string(), value.to_string());
}
} else if !token.is_empty() {
attrs.insert(token.to_string(), String::new());
}
}
attrs
}
pub fn find_code_ranges(doc: &str) -> Vec<(usize, usize)> {
let t = std::time::Instant::now();
let mut ranges = Vec::new();
let parser = Parser::new_ext(doc, Options::empty());
let mut iter = parser.into_offset_iter();
while let Some((event, range)) = iter.next() {
match event {
Event::Code(_) => {
ranges.push((range.start, range.end));
}
Event::Start(Tag::CodeBlock(_)) => {
let block_start = range.start;
let mut block_end = range.end;
for (inner_event, inner_range) in iter.by_ref() {
block_end = inner_range.end;
if matches!(inner_event, Event::End(TagEnd::CodeBlock)) {
break;
}
}
ranges.push((block_start, block_end));
}
_ => {}
}
}
let elapsed = t.elapsed().as_millis();
if elapsed > 0 {
eprintln!("[perf] find_code_ranges: {}ms", elapsed);
}
ranges
}
pub fn find_non_agent_html_comment_ranges(doc: &str) -> Vec<(usize, usize)> {
let code_ranges = find_code_ranges(doc);
let in_code = |pos: usize| {
code_ranges
.iter()
.any(|&(start, end)| pos >= start && pos < end)
};
let bytes = doc.as_bytes();
let len = bytes.len();
let mut ranges = Vec::new();
let mut pos = 0usize;
while pos + 4 <= len {
if &bytes[pos..pos + 4] != b"<!--" {
pos += 1;
continue;
}
if in_code(pos) {
pos += 4;
continue;
}
let Some(end) = find_comment_end(bytes, pos + 4) else {
let inner = &doc[pos + 4..];
if !is_agent_marker(inner) && !inner.trim_start().starts_with("agent:boundary:") {
ranges.push((pos, len));
break;
}
pos += 4;
continue;
};
let inner = &doc[pos + 4..end - 3];
if !is_agent_marker(inner) && !inner.trim().starts_with("agent:boundary:") {
ranges.push((pos, end));
}
pos = end;
}
ranges
}
pub fn parse(doc: &str) -> Result<Vec<Component>> {
let bytes = doc.as_bytes();
let len = bytes.len();
let code_ranges = find_code_ranges(doc);
let mut templates: Vec<Component> = Vec::new();
let mut stack: Vec<(String, HashMap<String, String>, usize, usize)> = Vec::new();
let mut pos = 0;
while pos + 4 <= len {
if &bytes[pos..pos + 4] != b"<!--" {
pos += 1;
continue;
}
if code_ranges
.iter()
.any(|&(start, end)| pos >= start && pos < end)
{
pos += 4;
continue;
}
let peek_start = pos + 4;
let peek = bounded_preview(doc, peek_start, 20);
let peek_trimmed = peek.trim_start();
if !peek_trimmed.starts_with("agent:") && !peek_trimmed.starts_with("/agent:") {
pos += 1;
continue;
}
let marker_start = pos;
let close = match find_comment_end(bytes, pos + 4) {
Some(c) => c,
None => {
pos += 4;
continue;
}
};
let inner = &doc[marker_start + 4..close - 3]; let trimmed = inner.trim();
let mut marker_end = close;
if marker_end < len && bytes[marker_end] == b'\n' {
marker_end += 1;
}
if let Some(name) = trimmed.strip_prefix("/agent:") {
if !is_valid_name(name) {
bail!("invalid component name: '{}'", name);
}
match stack.pop() {
Some((open_name, open_attrs, open_start, open_end)) => {
if open_name != name {
bail!(
"mismatched component: opened '{}' but closed '{}'",
open_name,
name
);
}
templates.push(Component {
name: name.to_string(),
attrs: open_attrs,
open_start,
open_end,
close_start: marker_start,
close_end: marker_end,
});
}
None => bail!(
"closing marker <!-- /agent:{} --> without matching open",
name
),
}
} else if let Some(rest) = trimmed.strip_prefix("agent:") {
if rest.starts_with("boundary:") {
pos = close;
continue;
}
let mut parts = rest.splitn(2, |c: char| c.is_whitespace());
let name = parts.next().unwrap_or("");
let attr_text = parts.next().unwrap_or("");
if !is_valid_name(name) {
bail!("invalid component name: '{}'", name);
}
let attrs = parse_attrs(attr_text);
stack.push((name.to_string(), attrs, marker_start, marker_end));
}
pos = close;
}
if let Some((name, _, _, _)) = stack.last() {
bail!(
"unclosed component: <!-- agent:{} --> without matching close",
name
);
}
templates.sort_by_key(|t| t.open_start);
Ok(templates)
}
pub fn find_comment_end(bytes: &[u8], start: usize) -> Option<usize> {
let len = bytes.len();
let mut i = start;
while i + 3 <= len {
if &bytes[i..i + 3] == b"-->" {
return Some(i + 3);
}
i += 1;
}
None
}
pub fn strip_comments(content: &str) -> String {
let code_ranges = find_code_ranges(content);
let in_code = |pos: usize| {
code_ranges
.iter()
.any(|&(start, end)| pos >= start && pos < end)
};
let mut result = String::with_capacity(content.len());
let bytes = content.as_bytes();
let len = bytes.len();
let mut pos = 0;
while pos < len {
if bytes[pos] == b'['
&& !in_code(pos)
&& is_line_start_at(bytes, pos)
&& let Some(end) = match_link_ref_comment_at(bytes, pos)
{
pos = end;
continue;
}
if pos + 4 <= len
&& &bytes[pos..pos + 4] == b"<!--"
&& !in_code(pos)
&& let Some((end, inner)) = match_html_comment_at(content, pos)
{
if is_agent_marker(inner) {
result.push_str(&content[pos..end]);
pos = end;
} else {
let mut skip_to = end;
if is_line_start_at(bytes, pos) && skip_to < len && bytes[skip_to] == b'\n' {
skip_to += 1;
}
pos = skip_to;
}
continue;
}
result.push(content[pos..].chars().next().unwrap());
pos += content[pos..].chars().next().unwrap().len_utf8();
}
result
}
fn is_line_start_at(bytes: &[u8], pos: usize) -> bool {
pos == 0 || bytes[pos - 1] == b'\n'
}
fn match_link_ref_comment_at(bytes: &[u8], pos: usize) -> Option<usize> {
let prefix = b"[//]: # (";
let len = bytes.len();
if pos + prefix.len() > len || &bytes[pos..pos + prefix.len()] != prefix {
return None;
}
let mut i = pos + prefix.len();
while i < len && bytes[i] != b')' && bytes[i] != b'\n' {
i += 1;
}
if i < len && bytes[i] == b')' {
i += 1;
if i < len && bytes[i] == b'\n' {
i += 1;
}
Some(i)
} else {
None
}
}
fn match_html_comment_at(content: &str, pos: usize) -> Option<(usize, &str)> {
let bytes = content.as_bytes();
let len = bytes.len();
let mut i = pos + 4;
while i + 3 <= len {
if &bytes[i..i + 3] == b"-->" {
let inner = &content[pos + 4..i];
return Some((i + 3, inner));
}
i += 1;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_range() {
let doc = "before\n<!-- agent:status -->\nHello\n<!-- /agent:status -->\nafter\n";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "status");
assert_eq!(ranges[0].content(doc), "Hello\n");
}
#[test]
fn nested_ranges() {
let doc = "\
<!-- agent:outer -->
<!-- agent:inner -->
content
<!-- /agent:inner -->
<!-- /agent:outer -->
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 2);
assert_eq!(ranges[0].name, "outer");
assert_eq!(ranges[1].name, "inner");
assert_eq!(ranges[1].content(doc), "content\n");
}
#[test]
fn siblings() {
let doc = "\
<!-- agent:a -->
alpha
<!-- /agent:a -->
<!-- agent:b -->
beta
<!-- /agent:b -->
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 2);
assert_eq!(ranges[0].name, "a");
assert_eq!(ranges[0].content(doc), "alpha\n");
assert_eq!(ranges[1].name, "b");
assert_eq!(ranges[1].content(doc), "beta\n");
}
#[test]
fn no_ranges() {
let doc = "# Just a document\n\nWith no range templates.\n";
let ranges = parse(doc).unwrap();
assert!(ranges.is_empty());
}
#[test]
fn unmatched_open_error() {
let doc = "<!-- agent:orphan -->\nContent\n";
let err = parse(doc).unwrap_err();
assert!(err.to_string().contains("unclosed component"));
}
#[test]
fn unmatched_close_error() {
let doc = "Content\n<!-- /agent:orphan -->\n";
let err = parse(doc).unwrap_err();
assert!(err.to_string().contains("without matching open"));
}
#[test]
fn mismatched_names_error() {
let doc = "<!-- agent:foo -->\n<!-- /agent:bar -->\n";
let err = parse(doc).unwrap_err();
assert!(err.to_string().contains("mismatched"));
}
#[test]
fn invalid_name() {
let doc = "<!-- agent:-bad -->\n<!-- /agent:-bad -->\n";
let err = parse(doc).unwrap_err();
assert!(err.to_string().contains("invalid component name"));
}
#[test]
fn name_validation() {
assert!(is_valid_name("status"));
assert!(is_valid_name("my-section"));
assert!(is_valid_name("a1"));
assert!(is_valid_name("A"));
assert!(!is_valid_name(""));
assert!(!is_valid_name("-bad"));
assert!(!is_valid_name("has space"));
assert!(!is_valid_name("has_underscore"));
}
#[test]
fn content_extraction() {
let doc = "<!-- agent:x -->\nfoo\nbar\n<!-- /agent:x -->\n";
let ranges = parse(doc).unwrap();
assert_eq!(ranges[0].content(doc), "foo\nbar\n");
}
#[test]
fn replace_roundtrip() {
let doc = "before\n<!-- agent:s -->\nold\n<!-- /agent:s -->\nafter\n";
let ranges = parse(doc).unwrap();
let new_doc = ranges[0].replace_content(doc, "new\n");
assert_eq!(
new_doc,
"before\n<!-- agent:s -->\nnew\n<!-- /agent:s -->\nafter\n"
);
let ranges2 = parse(&new_doc).unwrap();
assert_eq!(ranges2.len(), 1);
assert_eq!(ranges2[0].content(&new_doc), "new\n");
}
#[test]
fn is_agent_marker_yes() {
assert!(is_agent_marker(" agent:status "));
assert!(is_agent_marker("/agent:status"));
assert!(is_agent_marker("agent:my-thing"));
assert!(is_agent_marker(" /agent:A1 "));
}
#[test]
fn is_agent_marker_no() {
assert!(!is_agent_marker("just a comment"));
assert!(!is_agent_marker("agent:"));
assert!(!is_agent_marker("/agent:"));
assert!(!is_agent_marker("agent:-bad"));
assert!(!is_agent_marker("some agent:fake stuff"));
}
#[test]
fn regular_comments_ignored() {
let doc = "<!-- just a comment -->\n<!-- agent:x -->\ndata\n<!-- /agent:x -->\n";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "x");
}
#[test]
fn regular_comments_with_multibyte_preview_boundary_ignored() {
let doc = "\
<!-- 123456789012345678❯ ordinary comment -->
<!-- agent:x -->
data
<!-- /agent:x -->
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "x");
}
#[test]
fn multiline_comment_ignored() {
let doc = "\
<!--
multi
line
comment
-->
<!-- agent:s -->
content
<!-- /agent:s -->
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "s");
}
#[test]
fn empty_content() {
let doc = "<!-- agent:empty --><!-- /agent:empty -->\n";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].content(doc), "");
}
#[test]
fn markers_in_fenced_code_block_ignored() {
let doc = "\
<!-- agent:real -->
content
<!-- /agent:real -->
```markdown
<!-- agent:fake -->
this is just an example
<!-- /agent:fake -->
```
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "real");
}
#[test]
fn markers_in_inline_code_ignored() {
let doc = "\
Use `<!-- agent:example -->` markers for components.
<!-- agent:real -->
content
<!-- /agent:real -->
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "real");
}
#[test]
fn markers_in_tilde_fence_ignored() {
let doc = "\
<!-- agent:x -->
data
<!-- /agent:x -->
~~~
<!-- agent:y -->
example
<!-- /agent:y -->
~~~
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "x");
}
#[test]
fn markers_in_indented_fenced_code_block_ignored() {
let doc = "\
<!-- agent:exchange -->
Content here.
<!-- /agent:exchange -->
```markdown
<!-- agent:fake -->
demo without closing tag
```
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "exchange");
}
#[test]
fn indented_fence_inside_component_ignored() {
let doc = "\
<!-- agent:exchange -->
Here's how to set up:
```markdown
<!-- agent:status -->
Your status here
```
Done explaining.
<!-- /agent:exchange -->
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "exchange");
}
#[test]
fn deeply_indented_fence_ignored() {
let doc = "\
<!-- agent:x -->
ok
<!-- /agent:x -->
```
<!-- agent:y -->
inside fence
```
";
let ranges = parse(doc).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].name, "x");
}
#[test]
fn indented_fence_code_ranges_detected() {
let doc = "before\n ```\n code\n ```\nafter\n";
let ranges = find_code_ranges(doc);
assert_eq!(ranges.len(), 1);
assert!(doc[ranges[0].0..ranges[0].1].contains("code"));
}
#[test]
fn code_ranges_detected() {
let doc = "before\n```\ncode\n```\nafter `inline` end\n";
let ranges = find_code_ranges(doc);
assert_eq!(ranges.len(), 2);
assert!(doc[ranges[0].0..ranges[0].1].contains("code"));
assert!(doc[ranges[1].0..ranges[1].1].contains("inline"));
}
#[test]
fn code_ranges_double_backtick() {
let doc = "text `` `<!--` `` more\n";
let ranges = find_code_ranges(doc);
assert_eq!(ranges.len(), 1);
let span = &doc[ranges[0].0..ranges[0].1];
assert!(
span.contains("<!--"),
"double-backtick span should contain <!--: {:?}",
span
);
}
#[test]
fn code_ranges_double_backtick_does_not_match_single() {
let doc = "text `` foo ` bar `` end\n";
let ranges = find_code_ranges(doc);
assert_eq!(ranges.len(), 1);
let span = &doc[ranges[0].0..ranges[0].1];
assert_eq!(span, "`` foo ` bar ``");
}
#[test]
fn double_backtick_comment_before_agent_marker() {
let doc = "\
<!-- agent:exchange -->\n\
text `` `<!--` `` description\n\
new content here\n\
<!-- /agent:exchange -->\n";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].name, "exchange");
assert!(components[0].content(doc).contains("new content here"));
}
#[test]
fn parse_component_with_mode_attr() {
let doc = "<!-- agent:exchange mode=append -->\nContent\n<!-- /agent:exchange -->\n";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].name, "exchange");
assert_eq!(
components[0].attrs.get("mode").map(|s| s.as_str()),
Some("append")
);
assert_eq!(components[0].content(doc), "Content\n");
}
#[test]
fn parse_component_with_multiple_attrs() {
let doc = "<!-- agent:log mode=prepend timestamp=true -->\nData\n<!-- /agent:log -->\n";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].name, "log");
assert_eq!(
components[0].attrs.get("mode").map(|s| s.as_str()),
Some("prepend")
);
assert_eq!(
components[0].attrs.get("timestamp").map(|s| s.as_str()),
Some("true")
);
}
#[test]
fn parse_component_no_attrs_backward_compat() {
let doc = "<!-- agent:status -->\nOK\n<!-- /agent:status -->\n";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].name, "status");
assert!(components[0].attrs.is_empty());
}
#[test]
fn is_agent_marker_with_attrs() {
assert!(is_agent_marker(" agent:exchange mode=append "));
assert!(is_agent_marker("agent:status mode=replace"));
assert!(is_agent_marker("agent:log mode=prepend timestamp=true"));
}
#[test]
fn closing_tag_unchanged_with_attrs() {
let doc = "<!-- agent:status mode=replace -->\n- [x] Done\n<!-- /agent:status -->\n";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
let new_doc = components[0].replace_content(doc, "- [ ] Todo\n");
assert!(new_doc.contains("<!-- agent:status mode=replace -->"));
assert!(new_doc.contains("<!-- /agent:status -->"));
assert!(new_doc.contains("- [ ] Todo"));
}
#[test]
fn parse_component_with_patch_attr() {
let doc = "<!-- agent:exchange patch=append -->\nContent\n<!-- /agent:exchange -->\n";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].name, "exchange");
assert_eq!(components[0].patch_mode(), Some("append"));
assert_eq!(components[0].content(doc), "Content\n");
}
#[test]
fn patch_attr_takes_precedence_over_mode() {
let doc = "<!-- agent:exchange patch=replace mode=append -->\nContent\n<!-- /agent:exchange -->\n";
let components = parse(doc).unwrap();
assert_eq!(components[0].patch_mode(), Some("replace"));
}
#[test]
fn mode_attr_backward_compat() {
let doc = "<!-- agent:exchange mode=append -->\nContent\n<!-- /agent:exchange -->\n";
let components = parse(doc).unwrap();
assert_eq!(components[0].patch_mode(), Some("append"));
}
#[test]
fn no_patch_or_mode_attr() {
let doc = "<!-- agent:exchange -->\nContent\n<!-- /agent:exchange -->\n";
let components = parse(doc).unwrap();
assert_eq!(components[0].patch_mode(), None);
}
#[test]
fn single_backtick_component_tag_ignored() {
let doc = "\
Use `<!-- agent:pending -->` to mark pending sections.
<!-- agent:real -->
content
<!-- /agent:real -->
";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].name, "real");
}
#[test]
fn double_backtick_component_tag_ignored() {
let doc = "\
Use ``<!-- agent:pending -->`` to mark pending sections.
<!-- agent:real -->
content
<!-- /agent:real -->
";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].name, "real");
}
#[test]
fn component_tags_not_in_backticks_still_work() {
let doc = "\
<!-- agent:a -->
alpha
<!-- /agent:a -->
<!-- agent:b patch=append -->
beta
<!-- /agent:b -->
";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 2);
assert_eq!(components[0].name, "a");
assert_eq!(components[1].name, "b");
assert_eq!(components[1].patch_mode(), Some("append"));
}
#[test]
fn mixed_backtick_and_real_tags() {
let doc = "\
Here is an example: `<!-- agent:fake -->` and ``<!-- /agent:fake -->``.
<!-- agent:real -->
real content
<!-- /agent:real -->
Another example: `<!-- agent:also-fake patch=replace -->` is just documentation.
";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].name, "real");
assert_eq!(components[0].content(doc), "real content\n");
}
#[test]
fn inline_code_mid_line_with_surrounding_text_ignored() {
let doc = "\
Wrap markers like `<!-- agent:status -->` in backticks to show them literally.
<!-- agent:real -->
actual content
<!-- /agent:real -->
";
let components = parse(doc).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].name, "real");
assert_eq!(components[0].content(doc), "actual content\n");
}
#[test]
fn parse_attrs_unit() {
let attrs = parse_attrs("mode=append");
assert_eq!(attrs.get("mode").map(|s| s.as_str()), Some("append"));
let attrs = parse_attrs("mode=replace timestamp=true");
assert_eq!(attrs.len(), 2);
let attrs = parse_attrs("");
assert!(attrs.is_empty());
let attrs = parse_attrs("mode=append broken novalue=");
assert_eq!(attrs.len(), 2);
assert_eq!(attrs.get("mode").map(|s| s.as_str()), Some("append"));
assert_eq!(attrs.get("broken").map(|s| s.as_str()), Some(""));
let attrs = parse_attrs("auto");
assert_eq!(attrs.len(), 1);
assert!(attrs.contains_key("auto"));
}
#[test]
fn append_with_boundary_skips_code_block() {
let boundary_id = "real-uuid";
let doc = format!(
"<!-- agent:exchange patch=append -->\n\
user prompt\n\
```\n\
<!-- agent:boundary:{boundary_id} -->\n\
```\n\
more user text\n\
<!-- agent:boundary:{boundary_id} -->\n\
<!-- /agent:exchange -->\n"
);
let components = parse(&doc).unwrap();
let comp = &components[0];
let result =
comp.append_with_boundary(&doc, "### Re: Response\n\nContent here.", boundary_id);
assert!(result.contains("### Re: Response"));
assert!(result.contains("more user text"));
assert!(result.contains(&format!("<!-- agent:boundary:{boundary_id} -->\n```")));
assert!(!result.contains(&format!(
"more user text\n<!-- agent:boundary:{boundary_id} -->\n<!-- /agent:exchange -->"
)));
}
#[test]
fn append_with_boundary_no_code_block() {
let boundary_id = "simple-uuid";
let doc = format!(
"<!-- agent:exchange patch=append -->\n\
user prompt\n\
<!-- agent:boundary:{boundary_id} -->\n\
<!-- /agent:exchange -->\n"
);
let components = parse(&doc).unwrap();
let comp = &components[0];
let result = comp.append_with_boundary(&doc, "### Re: Answer\n\nDone.", boundary_id);
assert!(result.contains("### Re: Answer"));
assert!(result.contains("user prompt"));
assert!(!result.contains(&format!("agent:boundary:{boundary_id}")));
assert!(result.contains("agent:boundary:"));
}
#[test]
fn append_with_boundary_skips_already_present_content() {
let boundary_id = "simple-uuid";
let doc = format!(
"<!-- agent:exchange patch=append -->\n\
### Re: Duplicate — gpt-5 (HEAD)\n\
\n\
Already applied.\n\
<!-- agent:boundary:old-id -->\n\
<!-- agent:boundary:{boundary_id} -->\n\
<!-- /agent:exchange -->\n"
);
let components = parse(&doc).unwrap();
let comp = &components[0];
let result = comp.append_with_boundary(
&doc,
"### Re: Duplicate — gpt-5\n\nAlready applied.\n",
boundary_id,
);
assert_eq!(result, doc);
}
#[test]
fn append_with_caret_skips_already_present_content() {
let doc = "<!-- agent:exchange patch=append -->\n\
User prompt.\n\
### Re: Duplicate — gpt-5\n\
\n\
Already applied.\n\
<!-- /agent:exchange -->\n";
let components = parse(doc).unwrap();
let comp = &components[0];
let result =
comp.append_with_caret(doc, "### Re: Duplicate — gpt-5\n\nAlready applied.\n", None);
assert_eq!(result, doc);
}
#[test]
fn strip_comments_removes_html_comment() {
let result = strip_comments("before\n<!-- a comment -->\nafter\n");
assert_eq!(result, "before\nafter\n");
}
#[test]
fn non_agent_html_comment_ranges_cover_multiline_body() {
let doc = concat!(
"before\n",
"<!--\n",
"do #hidden. spec-test-build-install-commit-push\n",
"-->\n",
"<!-- agent:exchange -->\n",
"<!-- /agent:exchange -->\n"
);
let ranges = find_non_agent_html_comment_ranges(doc);
assert_eq!(ranges.len(), 1);
let hidden = doc.find("do #hidden").unwrap();
assert!(
ranges
.iter()
.any(|&(start, end)| hidden >= start && hidden < end),
"ordinary comment body should be inside a non-agent comment range"
);
assert!(
ranges
.iter()
.all(|&(start, end)| !doc[start..end].contains("agent:exchange")),
"agent component markers must not be treated as ordinary comments"
);
}
#[test]
fn non_agent_html_comment_ranges_cover_unterminated_tail() {
let doc = concat!(
"before\n",
"<!--\n",
"do #hidden. spec-test-build-install-commit-push\n",
"still typing\n"
);
let ranges = find_non_agent_html_comment_ranges(doc);
assert_eq!(ranges.len(), 1);
let (start, end) = ranges[0];
assert_eq!(
&doc[start..end],
"<!--\ndo #hidden. spec-test-build-install-commit-push\nstill typing\n"
);
assert_eq!(end, doc.len());
}
#[test]
fn strip_comments_preserves_agent_markers() {
let input = "text\n<!-- agent:status -->\ncontent\n<!-- /agent:status -->\n";
let result = strip_comments(input);
assert!(result.contains("<!-- agent:status -->"));
assert!(result.contains("<!-- /agent:status -->"));
}
#[test]
fn strip_comments_removes_link_ref() {
let result = strip_comments("[//]: # (hidden note)\nvisible\n");
assert_eq!(result, "visible\n");
}
#[test]
fn html_comment_in_content_does_not_eat_close_marker() {
let doc = "\
<!-- agent:pending -->
- [ ] [#abc] Rule: first line (not starting with ### or ❯ or <!-- ) gets prefix
<!-- /agent:pending -->
";
let comps = parse(doc).unwrap();
assert_eq!(comps.len(), 1);
assert_eq!(comps[0].name, "pending");
assert!(comps[0].content(doc).contains("<!-- )"));
}
#[test]
fn nested_html_comment_like_text_in_exchange() {
let doc = "\
<!-- agent:exchange -->
User typed <!-- some note --> in the text.
<!-- /agent:exchange -->
";
let comps = parse(doc).unwrap();
assert_eq!(comps.len(), 1);
assert_eq!(comps[0].name, "exchange");
}
#[test]
fn pending_item_with_literal_html_comment_opener() {
let doc = "\
<!-- agent:pending -->
- [ ] [#r7hw] Component parser: non-agent `<!-- ` in content ate close markers
- [ ] [#xyz] Another item
<!-- /agent:pending -->
";
let comps = parse(doc).unwrap();
assert_eq!(comps.len(), 1);
assert_eq!(comps[0].name, "pending");
let content = comps[0].content(doc);
assert!(content.contains("#r7hw"));
assert!(content.contains("#xyz"));
}
#[test]
fn multiple_non_agent_html_comments_in_pending() {
let doc = "\
<!-- agent:pending -->
- [ ] [#a] Fix rule: skip lines starting with <!-- or -->
- [ ] [#b] Handle <!-- partial comment
- [ ] [#c] Normal item
<!-- /agent:pending -->
";
let comps = parse(doc).unwrap();
assert_eq!(comps.len(), 1);
assert_eq!(comps[0].name, "pending");
let content = comps[0].content(doc);
assert!(content.contains("#a"));
assert!(content.contains("#b"));
assert!(content.contains("#c"));
}
#[test]
fn exchange_and_pending_both_with_html_comments_in_content() {
let doc = "\
<!-- agent:exchange -->
The rule checks for <!-- prefix before deciding.
### Re: topic — opus
Fix applied to skip non-agent <!-- sequences.
<!-- /agent:exchange -->
<!-- agent:pending -->
- [ ] [#cfdy] Verify <!-- in pending items
<!-- /agent:pending -->
";
let comps = parse(doc).unwrap();
assert_eq!(comps.len(), 2);
assert_eq!(comps[0].name, "exchange");
assert_eq!(comps[1].name, "pending");
assert!(comps[0].content(doc).contains("<!-- prefix"));
assert!(comps[1].content(doc).contains("#cfdy"));
}
#[test]
fn is_backlog_component_accepts_both_names() {
assert!(is_backlog_component("backlog"));
assert!(is_backlog_component("pending"));
assert!(!is_backlog_component("exchange"));
assert!(!is_backlog_component("status"));
assert!(!is_backlog_component("pending-done"));
assert!(!is_backlog_component("backlog-done"));
assert!(!is_backlog_component("done"));
}
#[test]
fn is_backlog_done_component_accepts_only_canonical_done() {
assert!(is_backlog_done_component("done"));
assert!(!is_backlog_done_component("backlog-done"));
assert!(!is_backlog_done_component("pending-done"));
assert!(!is_backlog_done_component("backlog"));
assert!(!is_backlog_done_component("pending"));
assert!(!is_backlog_done_component("icebox"));
}
#[test]
fn is_review_component_accepts_name() {
assert!(is_review_component("review"));
assert!(!is_review_component("backlog"));
assert!(!is_review_component("icebox"));
}
#[test]
fn is_icebox_component_accepts_name() {
assert!(is_icebox_component("icebox"));
assert!(!is_icebox_component("backlog"));
assert!(!is_icebox_component("exchange"));
assert!(!is_icebox_component("icebox-archive"));
}
#[test]
fn tracked_work_component_includes_review() {
assert!(is_tracked_work_component("backlog"));
assert!(is_tracked_work_component("pending"));
assert!(is_tracked_work_component("review"));
assert!(is_tracked_work_component("icebox"));
assert!(!is_tracked_work_component("done"));
}
#[test]
fn icebox_component_parsed() {
let doc = "\
<!-- agent:icebox -->
- Parked idea
<!-- /agent:icebox -->
";
let comps = parse(doc).unwrap();
assert_eq!(comps.len(), 1);
assert_eq!(comps[0].name, "icebox");
assert!(is_icebox_component(&comps[0].name));
assert!(comps[0].content(doc).contains("Parked idea"));
}
#[test]
fn backlog_component_parsed_from_new_marker() {
let doc = "\
<!-- agent:backlog -->
- [ ] [#abc] First item
<!-- /agent:backlog -->
";
let comps = parse(doc).unwrap();
assert_eq!(comps.len(), 1);
assert_eq!(comps[0].name, "backlog");
assert!(is_backlog_component(&comps[0].name));
assert!(comps[0].content(doc).contains("#abc"));
}
#[test]
fn legacy_pending_marker_still_parsed() {
let doc = "\
<!-- agent:pending -->
- [ ] [#xyz] Legacy item
<!-- /agent:pending -->
";
let comps = parse(doc).unwrap();
assert_eq!(comps.len(), 1);
assert_eq!(comps[0].name, "pending");
assert!(is_backlog_component(&comps[0].name));
}
#[test]
fn strip_backlog_patch_attr_removes_patch_replace() {
let doc = "<!-- agent:backlog patch=replace -->\n- item\n<!-- /agent:backlog -->\n";
let result = strip_backlog_patch_attr(doc);
assert_eq!(
result,
"<!-- agent:backlog -->\n- item\n<!-- /agent:backlog -->\n"
);
}
#[test]
fn strip_backlog_patch_attr_removes_pending_patch_replace() {
let doc = "<!-- agent:pending patch=replace -->\n- item\n<!-- /agent:pending -->\n";
let result = strip_backlog_patch_attr(doc);
assert_eq!(
result,
"<!-- agent:pending -->\n- item\n<!-- /agent:pending -->\n"
);
}
#[test]
fn strip_backlog_patch_attr_removes_mode_replace() {
let doc = "<!-- agent:backlog mode=replace -->\n- item\n<!-- /agent:backlog -->\n";
let result = strip_backlog_patch_attr(doc);
assert_eq!(
result,
"<!-- agent:backlog -->\n- item\n<!-- /agent:backlog -->\n"
);
}
#[test]
fn strip_backlog_patch_attr_preserves_other_attrs() {
let doc =
"<!-- agent:backlog patch=replace max_lines=50 -->\n- item\n<!-- /agent:backlog -->\n";
let result = strip_backlog_patch_attr(doc);
assert_eq!(
result,
"<!-- agent:backlog max_lines=50 -->\n- item\n<!-- /agent:backlog -->\n"
);
}
#[test]
fn strip_backlog_patch_attr_noop_for_exchange() {
let doc = "<!-- agent:exchange patch=append -->\ncontent\n<!-- /agent:exchange -->\n";
let result = strip_backlog_patch_attr(doc);
assert_eq!(result, doc);
}
#[test]
fn strip_backlog_patch_attr_noop_when_no_attr() {
let doc = "<!-- agent:backlog -->\n- item\n<!-- /agent:backlog -->\n";
let result = strip_backlog_patch_attr(doc);
assert_eq!(result, doc);
}
}