shared-context-engineering 0.3.0

Shared Context Engineering CLI
use super::{
    build_agent_trace, patches_have_overlap, validate_agent_trace_value, AgentTraceMetadataInput,
    AgentTraceVcsType, LineRange, AGENT_TRACE_VERSION,
};
use crate::services::{
    agent_trace::agent_trace_conversation_url,
    patch::{combine_patches, parse_patch, ParsedPatch},
    structured_patch::{derive_claude_structured_patch, ClaudeStructuredPatchDerivationResult},
};
use serde_json::{json, Value};

#[derive(Clone, Copy)]
struct AgentTraceScenario {
    incremental: &'static [&'static str],
    post_commit: &'static str,
    golden: &'static str,
}

const TEST_COMMIT_TIMESTAMP: &str = "2026-04-23T10:20:30Z";
const TEST_COMMIT_REVISION: &str = "a0b1c2d3e4f5a6b7c8d9e0f11223344556677889";

fn parse_fixtures(fixtures: &[&str]) -> Vec<ParsedPatch> {
    fixtures
        .iter()
        .map(|fixture| parse_patch(fixture, None).expect("fixture patch should parse"))
        .collect()
}

fn parse_fixture(fixture: &str) -> ParsedPatch {
    parse_patch(fixture, None).expect("fixture patch should parse")
}

const TEXT_FILE_LIFECYCLE_RECONSTRUCTION_INCREMENTALS: &[&str] = &[
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_01.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_02.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_03.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_04.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_05.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_06.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_07.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_08.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_09.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_10.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_11.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_12.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_13.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_14.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_15.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_16.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_17.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_18.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_19.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_20.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_21.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_22.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_23.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_24.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_25.patch"),
    include_str!("fixtures/text_file_lifecycle_reconstruction/incremental_26.patch"),
];

fn assert_builds_expected_agent_trace(scenario: AgentTraceScenario) {
    let constructed_patch = combine_patches(&parse_fixtures(scenario.incremental));
    let post_commit_patch =
        parse_patch(scenario.post_commit, None).expect("fixture patch should parse");
    let golden: Value = serde_json::from_str(scenario.golden).expect("golden json should load");
    validate_agent_trace_value(&golden).expect("golden json should validate against schema");
    let actual = build_agent_trace(
        &constructed_patch,
        &post_commit_patch,
        AgentTraceMetadataInput {
            commit_timestamp: TEST_COMMIT_TIMESTAMP,
            commit_revision: TEST_COMMIT_REVISION,
            vcs_type: Some(AgentTraceVcsType::Git),
            tool_name: None,
            tool_version: None,
        },
    )
    .expect("agent trace should build");
    assert_eq!(actual.version, AGENT_TRACE_VERSION);
    assert_eq!(actual.timestamp, TEST_COMMIT_TIMESTAMP);
    assert_eq!(
        actual.vcs,
        Some(super::AgentTraceVcs {
            r#type: AgentTraceVcsType::Git,
            revision: TEST_COMMIT_REVISION.to_string(),
        })
    );
    let actual_json = serde_json::to_value(&actual).expect("agent trace should serialize");
    validate_agent_trace_value(&actual_json).expect("actual json should validate against schema");
    let expected_conversation_url = agent_trace_conversation_url(&actual.id);
    let mut expected_files = golden["files"].clone();
    for conversation in expected_files
        .as_array_mut()
        .expect("golden files should be an array")
        .iter_mut()
        .flat_map(|file| {
            file["conversations"]
                .as_array_mut()
                .expect("golden conversations should be an array")
                .iter_mut()
        })
    {
        conversation["url"] = Value::String(expected_conversation_url.clone());
    }
    let metadata_version = actual_json["metadata"]["sce"]["version"]
        .as_str()
        .expect("metadata.sce.version should serialize as a string");
    assert!(
        !metadata_version.is_empty(),
        "metadata.sce.version should not be empty"
    );
    assert_eq!(actual_json["vcs"], golden["vcs"]);
    assert_eq!(actual_json["files"], expected_files);
}

#[test]
fn patch_overlap_predicate_detects_matching_touched_lines() {
    let candidate_patch = parse_fixture(include_str!(
        "fixtures/hello_world_reconstruction/incremental_01.patch"
    ));
    let target_patch = parse_fixture(include_str!(
        "fixtures/hello_world_reconstruction/post_commit.patch"
    ));

    assert!(patches_have_overlap(&candidate_patch, &target_patch));
}

#[test]
fn patch_overlap_predicate_rejects_unrelated_touched_lines() {
    let candidate_patch = parse_fixture(include_str!(
        "fixtures/hello_world_reconstruction/incremental_01.patch"
    ));
    let target_patch = parse_fixture(include_str!(
        "fixtures/poem_write_reconstruction/post_commit.patch"
    ));

    assert!(!patches_have_overlap(&candidate_patch, &target_patch));
}

#[test]
fn patch_overlap_predicate_rejects_empty_or_untouched_patches() {
    let candidate_patch = parse_fixture(include_str!(
        "fixtures/hello_world_reconstruction/incremental_01.patch"
    ));
    let untouched_patch = parse_fixture(include_str!(
        "../structured_patch/fixtures/write_create_empty/expected.patch"
    ));
    let empty_patch = parse_fixture("");

    assert!(!patches_have_overlap(&candidate_patch, &untouched_patch));
    assert!(!patches_have_overlap(&untouched_patch, &candidate_patch));
    assert!(!patches_have_overlap(&empty_patch, &candidate_patch));
    assert!(!patches_have_overlap(&candidate_patch, &empty_patch));
}

#[test]
fn patch_overlap_predicate_accepts_claude_structured_patch_derivation() {
    let payload: Value = serde_json::from_str(include_str!(
        "../structured_patch/fixtures/edit_single_hunk/claude-post-tool-use.json"
    ))
    .expect("Claude structured fixture should parse");
    let expected_patch = parse_fixture(include_str!(
        "../structured_patch/fixtures/edit_single_hunk/expected.patch"
    ));
    let derived_patch = match derive_claude_structured_patch("PostToolUse", &payload, 1, None) {
        ClaudeStructuredPatchDerivationResult::Derived(derived) => derived.patch,
        ClaudeStructuredPatchDerivationResult::Skipped(reason) => {
            panic!("Claude structured fixture should derive a patch, got {reason}")
        }
    };

    assert_eq!(derived_patch, expected_patch);
    assert!(patches_have_overlap(&derived_patch, &expected_patch));
}

#[test]
fn average_age_reconstruction_matches_golden_agent_trace() {
    assert_builds_expected_agent_trace(AgentTraceScenario {
        incremental: &[
            include_str!("fixtures/average_age_reconstruction/incremental_01.patch"),
            include_str!("fixtures/average_age_reconstruction/incremental_02.patch"),
            include_str!("fixtures/average_age_reconstruction/incremental_03.patch"),
            include_str!("fixtures/average_age_reconstruction/incremental_04.patch"),
            include_str!("fixtures/average_age_reconstruction/incremental_05.patch"),
            include_str!("fixtures/average_age_reconstruction/incremental_06.patch"),
            include_str!("fixtures/average_age_reconstruction/incremental_07.patch"),
        ],
        post_commit: include_str!("fixtures/average_age_reconstruction/post_commit.patch"),
        golden: include_str!("fixtures/average_age_reconstruction/golden.json"),
    });
}

#[test]
fn hello_world_reconstruction_matches_golden_agent_trace() {
    assert_builds_expected_agent_trace(AgentTraceScenario {
        incremental: &[include_str!(
            "fixtures/hello_world_reconstruction/incremental_01.patch"
        )],
        post_commit: include_str!("fixtures/hello_world_reconstruction/post_commit.patch"),
        golden: include_str!("fixtures/hello_world_reconstruction/golden.json"),
    });
}

#[test]
fn mixed_change_reconstruction_matches_golden_agent_trace() {
    assert_builds_expected_agent_trace(AgentTraceScenario {
        incremental: &[
            include_str!("fixtures/mixed_change_reconstruction/incremental_01.patch"),
            include_str!("fixtures/mixed_change_reconstruction/incremental_02.patch"),
            include_str!("fixtures/mixed_change_reconstruction/incremental_03.patch"),
            include_str!("fixtures/mixed_change_reconstruction/incremental_04.patch"),
        ],
        post_commit: include_str!("fixtures/mixed_change_reconstruction/post_commit.patch"),
        golden: include_str!("fixtures/mixed_change_reconstruction/golden.json"),
    });
}

#[test]
fn poem_edit_reconstruction_matches_golden_agent_trace() {
    assert_builds_expected_agent_trace(AgentTraceScenario {
        incremental: &[
            include_str!("fixtures/poem_edit_reconstruction/incremental_01.patch"),
            include_str!("fixtures/poem_edit_reconstruction/incremental_02.patch"),
        ],
        post_commit: include_str!("fixtures/poem_edit_reconstruction/post_commit.patch"),
        golden: include_str!("fixtures/poem_edit_reconstruction/golden.json"),
    });
}

#[test]
fn poem_edit_reconstruction_maps_each_hunk_to_one_range() {
    let mut constructed_patch = combine_patches(&parse_fixtures(&[
        include_str!("fixtures/poem_edit_reconstruction/incremental_01.patch"),
        include_str!("fixtures/poem_edit_reconstruction/incremental_02.patch"),
    ]));
    let post_commit_patch = parse_patch(
        include_str!("fixtures/poem_edit_reconstruction/post_commit.patch"),
        None,
    )
    .expect("fixture patch should parse");

    let first_hunk_lines = &mut constructed_patch.files[0].hunks[0].lines;
    first_hunk_lines[0].session_id = Some(String::from("session-z"));
    first_hunk_lines[1].session_id = Some(String::from("session-a"));

    let agent_trace = build_agent_trace(
        &constructed_patch,
        &post_commit_patch,
        AgentTraceMetadataInput {
            commit_timestamp: TEST_COMMIT_TIMESTAMP,
            commit_revision: TEST_COMMIT_REVISION,
            vcs_type: Some(AgentTraceVcsType::Git),
            tool_name: None,
            tool_version: None,
        },
    )
    .expect("agent trace should build");

    let actual_json = serde_json::to_value(&agent_trace).expect("agent trace should serialize");
    validate_agent_trace_value(&actual_json).expect("actual json should validate against schema");

    assert_eq!(agent_trace.files.len(), 1);
    assert_eq!(agent_trace.files[0].path, "poem.md");
    assert_eq!(agent_trace.files[0].conversations.len(), 3);
    assert_eq!(
        agent_trace.files[0].conversations[0].related,
        Some(vec![
            super::ConversationRelated {
                kind: String::from("session"),
                url: String::from("https://sce.crocoder.dev/sessions/session-a"),
            },
            super::ConversationRelated {
                kind: String::from("session"),
                url: String::from("https://sce.crocoder.dev/sessions/session-z"),
            },
        ])
    );
    assert_eq!(agent_trace.files[0].conversations[1].related, None);
    assert_eq!(agent_trace.files[0].conversations[2].related, None);
    assert_eq!(
        actual_json["files"][0]["conversations"][0]["related"],
        json!([
            {
                "type": "session",
                "url": "https://sce.crocoder.dev/sessions/session-a"
            },
            {
                "type": "session",
                "url": "https://sce.crocoder.dev/sessions/session-z"
            }
        ])
    );
    assert!(
        actual_json["files"][0]["conversations"][1]["related"].is_null(),
        "conversations without session-backed lines should omit related"
    );
    assert!(
        actual_json["files"][0]["conversations"][2]["related"].is_null(),
        "conversations without session-backed lines should omit related"
    );
    assert_eq!(
        agent_trace.files[0]
            .conversations
            .iter()
            .map(|conversation| conversation.ranges.as_slice())
            .collect::<Vec<_>>(),
        vec![
            &[LineRange {
                start_line: 1,
                end_line: 8,
                content_hash: "murmur3:25e05a40".to_string(),
            }][..],
            &[LineRange {
                start_line: 10,
                end_line: 16,
                content_hash: "murmur3:bc5d346b".to_string(),
            }][..],
            &[LineRange {
                start_line: 21,
                end_line: 24,
                content_hash: "murmur3:c8621bcb".to_string(),
            }][..],
        ]
    );
}

#[test]
fn poem_write_reconstruction_matches_golden_agent_trace() {
    assert_builds_expected_agent_trace(AgentTraceScenario {
        incremental: &[include_str!(
            "fixtures/poem_write_reconstruction/incremental_01.patch"
        )],
        post_commit: include_str!("fixtures/poem_write_reconstruction/post_commit.patch"),
        golden: include_str!("fixtures/poem_write_reconstruction/golden.json"),
    });
}

#[test]
fn text_file_lifecycle_reconstruction_matches_golden_agent_trace() {
    assert_builds_expected_agent_trace(AgentTraceScenario {
        incremental: TEXT_FILE_LIFECYCLE_RECONSTRUCTION_INCREMENTALS,
        post_commit: include_str!("fixtures/text_file_lifecycle_reconstruction/post_commit.patch"),
        golden: include_str!("fixtures/text_file_lifecycle_reconstruction/golden.json"),
    });
}

#[test]
fn file_rename_reconstruction_matches_golden_agent_trace() {
    assert_builds_expected_agent_trace(AgentTraceScenario {
        incremental: &[include_str!(
            "fixtures/file_rename_reconstruction/incremental_01.patch"
        )],
        post_commit: include_str!("fixtures/file_rename_reconstruction/post_commit.patch"),
        golden: include_str!("fixtures/file_rename_reconstruction/golden.json"),
    });
}

#[test]
fn schema_validation_allows_agent_trace_without_vcs() {
    let value = json!({
        "version": AGENT_TRACE_VERSION,
        "id": "0196f25d-cf7f-7ca8-a652-8562c8a9f1d5",
        "timestamp": TEST_COMMIT_TIMESTAMP,
        "files": []
    });

    validate_agent_trace_value(&value)
        .expect("agent trace without vcs should validate against schema");
}

#[test]
fn schema_validation_rejects_vcs_missing_revision() {
    let value = json!({
        "version": AGENT_TRACE_VERSION,
        "id": "0196f25d-cf7f-7ca8-a652-8562c8a9f1d5",
        "timestamp": TEST_COMMIT_TIMESTAMP,
        "vcs": {
            "type": "git"
        },
        "files": []
    });

    let error = validate_agent_trace_value(&value)
        .expect_err("agent trace with vcs missing revision should fail validation");
    let rendered = error.to_string();
    assert!(
        rendered.contains("\"revision\" is a required property"),
        "expected vcs/revision validation failure, got: {rendered}"
    );
}