a73x

3f27d9bc

Accept PatchCreate/head_commit as aliases for backward-compatible event deserialization

a73x   2026-03-21 15:50

Worktree agents created events using a different format (PatchCreate tag + head_commit field)
than main expects (patch.create tag + branch field). Add serde aliases so both formats
deserialize correctly, and make resolve_head try OID parsing before branch lookup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/src/event.rs b/src/event.rs
index 515f884..d3e5a49 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -54,11 +54,12 @@ pub enum Action {
    },
    #[serde(rename = "issue.reopen")]
    IssueReopen,
    #[serde(rename = "patch.create")]
    #[serde(rename = "patch.create", alias = "PatchCreate")]
    PatchCreate {
        title: String,
        body: String,
        base_ref: String,
        #[serde(alias = "head_commit")]
        branch: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        fixes: Option<String>,
diff --git a/src/state.rs b/src/state.rs
index 8028f0b..05ae1a8 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -182,6 +182,13 @@ impl IssueState {
impl PatchState {
    /// Resolve the current head commit OID for this patch by looking up `refs/heads/{branch}`.
    pub fn resolve_head(&self, repo: &Repository) -> Result<Oid, crate::error::Error> {
        // Try parsing as a hex OID first (for patches created with head_commit)
        if let Ok(oid) = Oid::from_str(&self.branch) {
            if repo.find_commit(oid).is_ok() {
                return Ok(oid);
            }
        }
        // Fall back to branch name lookup
        let ref_name = format!("refs/heads/{}", self.branch);
        repo.refname_to_id(&ref_name).map_err(|e| {
            crate::error::Error::Cmd(format!(
diff --git a/tests/collab_test.rs b/tests/collab_test.rs
index 1a7461f..3b54981 100644
--- a/tests/collab_test.rs
+++ b/tests/collab_test.rs
@@ -831,6 +831,87 @@ fn test_patch_show_outdated_staleness() {
}

// ---------------------------------------------------------------------------
// Backward compat: head_commit alias for branch field
// ---------------------------------------------------------------------------

#[test]
fn test_patch_create_with_head_commit_field_deserializes() {
    // Events created with "head_commit" instead of "branch" should still work
    let json = r#"{
        "timestamp": "2026-03-21T00:00:00+00:00",
        "author": {"name": "agent", "email": "agent@test"},
        "action": {
            "type": "patch.create",
            "title": "Agent patch",
            "body": "from worktree",
            "base_ref": "main",
            "head_commit": "abc123def456"
        }
    }"#;
    let event: Event = serde_json::from_str(json).unwrap();
    match event.action {
        Action::PatchCreate { branch, .. } => {
            assert_eq!(branch, "abc123def456");
        }
        _ => panic!("expected PatchCreate"),
    }
}

#[test]
fn test_patch_create_with_branch_field_still_works() {
    let json = r#"{
        "timestamp": "2026-03-21T00:00:00+00:00",
        "author": {"name": "dev", "email": "dev@test"},
        "action": {
            "type": "patch.create",
            "title": "Normal patch",
            "body": "",
            "base_ref": "main",
            "branch": "feature-branch"
        }
    }"#;
    let event: Event = serde_json::from_str(json).unwrap();
    match event.action {
        Action::PatchCreate { branch, .. } => {
            assert_eq!(branch, "feature-branch");
        }
        _ => panic!("expected PatchCreate"),
    }
}

#[test]
fn test_resolve_head_with_oid_string() {
    // If branch field contains a hex OID, resolve_head should try to parse it as an OID
    let tmp = TempDir::new().unwrap();
    let repo = init_repo(tmp.path(), &alice());
    make_initial_commit(&repo, "main");
    let tip = add_commit_on_branch(&repo, "main", "f.rs", b"content");

    // Create a patch where "branch" is actually a commit OID string
    let sk = test_signing_key();
    let event = Event {
        timestamp: now(),
        author: alice(),
        action: Action::PatchCreate {
            title: "OID-based patch".to_string(),
            body: "".to_string(),
            base_ref: "main".to_string(),
            branch: tip.to_string(),
            fixes: None,
        },
        clock: 0,
    };
    let oid = dag::create_root_event(&repo, &event, &sk).unwrap();
    let id = oid.to_string();
    let ref_name = format!("refs/collab/patches/{}", id);
    repo.reference(&ref_name, oid, false, "test").unwrap();

    let state = PatchState::from_ref(&repo, &ref_name, &id).unwrap();
    let resolved = state.resolve_head(&repo).unwrap();
    assert_eq!(resolved, tip);
}

// ---------------------------------------------------------------------------
// Phase 5: US3 — Merge (T024-T025)
// ---------------------------------------------------------------------------