Feature: Diff review artifacts As a developer reviewing plan changes I want to build diff review artifacts from changesets So that I can inspect changes before applying them Background: Given a plan ID "01PLAN000000000000000001" And an empty changeset for the plan Scenario: Empty changeset produces empty review artifact When I build a review artifact from the changeset Then the review artifact should have 0 total changes And the review artifact should have 0 groups And the review artifact should not be truncated Scenario: Single CREATE change produces a diff entry Given a change entry with operation "create" for path "src/main.py" in resource "RES001" And after content for "src/main.py" is "print('hello')\n" When I build a review artifact from the changeset Then the review artifact should have 1 total changes And the review artifact should have 1 groups And group 0 should have 1 entries And entry 0 in group 0 should have change type "create" And entry 0 in group 0 should have path "src/main.py" Scenario: MODIFY change generates unified diff Given a change entry with operation "modify" for path "src/app.py" in resource "RES001" And before content for "src/app.py" is "x = 1\ny = 2\n" And after content for "src/app.py" is "x = 1\ny = 3\n" When I build a review artifact from the changeset Then entry 0 in group 0 should have change type "modify" And the diff text in entry 0 of group 0 should contain "---" And the diff text in entry 0 of group 0 should contain "+++" Scenario: DELETE change entry Given a change entry with operation "delete" for path "old.txt" in resource "RES001" And before content for "old.txt" is "old content\n" When I build a review artifact from the changeset Then entry 0 in group 0 should have change type "delete" And the diff text in entry 0 of group 0 should contain "---" Scenario: Multiple changes across different resources Given a change entry with operation "create" for path "a.py" in resource "RES001" And a change entry with operation "modify" for path "b.py" in resource "RES002" And after content for "a.py" is "new\n" And before content for "b.py" is "old\n" And after content for "b.py" is "new\n" When I build a review artifact from the changeset Then the review artifact should have 2 total changes And the review artifact should have 2 groups Scenario: Changes grouped by resource Given a change entry with operation "create" for path "a.py" in resource "RES001" And a change entry with operation "modify" for path "b.py" in resource "RES001" And after content for "a.py" is "new\n" And before content for "b.py" is "old\n" And after content for "b.py" is "new\n" When I build a review artifact from the changeset Then the review artifact should have 1 groups And group 0 should have 2 entries Scenario: Binary file detection Given a change entry with operation "create" for path "image.bin" in resource "RES001" And after content for "image.bin" is "binary\x00data" When I build a review artifact from the changeset Then entry 0 in group 0 should be binary And the diff text in entry 0 of group 0 should contain "[binary file]" Scenario: Rename detection with matching content hash Given a delete entry for path "old/file.py" in resource "RES001" with before_hash "abc123" And a create entry for path "new/file.py" in resource "RES001" with after_hash "abc123" When I build a review artifact from the changeset Then the review artifact should have 1 groups And group 0 should have 1 entries And entry 0 in group 0 should be a rename And entry 0 in group 0 rename should be from "old/file.py" to "new/file.py" And group 0 should have 1 renamed Scenario: Rename not detected when hashes differ Given a delete entry for path "old/file.py" in resource "RES001" with before_hash "abc123" And a create entry for path "new/file.py" in resource "RES001" with after_hash "xyz789" When I build a review artifact from the changeset Then group 0 should have 2 entries And entry 0 in group 0 should not be a rename Scenario: Max diff size truncation Given a change entry with operation "create" for path "big.py" in resource "RES001" And after content for "big.py" is a string of 100000 characters When I build a review artifact with max diff size 100 Then the review artifact should be truncated And entry 0 in group 0 should be truncated Scenario: Stable ordering by resource then path Given a change entry with operation "create" for path "z.py" in resource "RES002" And a change entry with operation "create" for path "a.py" in resource "RES001" And after content for "z.py" is "z\n" And after content for "a.py" is "a\n" When I build a review artifact from the changeset Then group 0 should have resource id "RES001" And group 1 should have resource id "RES002" Scenario: Before and after content hashes in metadata Given a hashed change entry "modify" path "f.py" resource "RES001" before_hash "hash1" after_hash "hash2" And before content for "f.py" is "old\n" And after content for "f.py" is "new\n" When I build a review artifact from the changeset Then entry 0 in group 0 should have before hash "hash1" And entry 0 in group 0 should have after hash "hash2" Scenario: File mode in metadata Given a moded change entry "modify" path "script.sh" resource "RES001" after_mode 0o755 And before content for "script.sh" is "#!/bin/sh\n" And after content for "script.sh" is "#!/bin/bash\n" When I build a review artifact from the changeset Then entry 0 in group 0 should have file mode 493 Scenario: Serialize to plain text Given a change entry with operation "create" for path "new.py" in resource "RES001" And after content for "new.py" is "hello\n" And resource name "RES001" is "my-resource" When I build a review artifact from the changeset And I serialize the artifact to plain text Then the plain text should contain "my-resource" And the plain text should contain "new.py" And the plain text should contain "Total changes: 1" Scenario: Serialize to JSON Given a change entry with operation "create" for path "new.py" in resource "RES001" And after content for "new.py" is "hello\n" When I build a review artifact from the changeset And I serialize the artifact to JSON Then the JSON output should be valid JSON And the JSON output should contain key "total_changes" And the JSON output should contain key "groups" Scenario: Serialize to rich format Given a change entry with operation "create" for path "new.py" in resource "RES001" And after content for "new.py" is "hello\n" When I build a review artifact from the changeset And I serialize the artifact to rich format Then the rich text should contain "[bold]" And the rich text should contain "new.py" Scenario: ResourceDiffGroup summary counts Given a change entry with operation "create" for path "a.py" in resource "RES001" And a change entry with operation "modify" for path "b.py" in resource "RES001" And a change entry with operation "delete" for path "c.py" in resource "RES001" And after content for "a.py" is "new\n" And before content for "b.py" is "old\n" And after content for "b.py" is "new\n" And before content for "c.py" is "old\n" When I build a review artifact from the changeset Then group 0 should have 1 added And group 0 should have 1 modified And group 0 should have 1 deleted Scenario: ReviewArtifact has plan and changeset IDs When I build a review artifact from the changeset Then the review artifact plan ID should match the changeset plan ID And the review artifact changeset ID should match the changeset ID Scenario: Empty content produces empty diff Given a change entry with operation "modify" for path "empty.py" in resource "RES001" When I build a review artifact from the changeset Then the diff text in entry 0 of group 0 should be empty Scenario: Multiple entries in same resource sorted by path Given a change entry with operation "create" for path "z.py" in resource "RES001" And a change entry with operation "create" for path "a.py" in resource "RES001" And after content for "z.py" is "z\n" And after content for "a.py" is "a\n" When I build a review artifact from the changeset Then entry 0 in group 0 should have path "a.py" And entry 1 in group 0 should have path "z.py" Scenario: Second entry fully truncated when budget exhausted Given a change entry with operation "create" for path "a.py" in resource "RES001" And a change entry with operation "create" for path "b.py" in resource "RES001" And after content for "a.py" is a string of 1000 characters And after content for "b.py" is a string of 1000 characters When I build a review artifact with max diff size 50 Then the review artifact should be truncated And entry 1 in group 0 should be truncated And the diff text in entry 1 of group 0 should contain "[truncated]" Scenario: JSON serialization includes hashes and file mode Given a hashed change entry "modify" path "f.py" resource "RES001" before_hash "aaa" after_hash "bbb" And a moded change entry "modify" path "g.py" resource "RES001" after_mode 0o644 And before content for "f.py" is "old\n" And after content for "f.py" is "new\n" And before content for "g.py" is "a\n" And after content for "g.py" is "b\n" When I build a review artifact from the changeset And I serialize the artifact to JSON Then the JSON output should be valid JSON And the JSON output should have entry with before hash "aaa" And the JSON output should have entry with after hash "bbb" And the JSON output should have entry with file mode Scenario: JSON serialization includes rename fields Given a delete entry for path "old/r.py" in resource "RES001" with before_hash "h1" And a create entry for path "new/r.py" in resource "RES001" with after_hash "h1" When I build a review artifact from the changeset And I serialize the artifact to JSON Then the JSON output should be valid JSON And the JSON output should have entry with rename from "old/r.py" Scenario: Rich format shows truncation warning Given a change entry with operation "create" for path "big.py" in resource "RES001" And after content for "big.py" is a string of 100000 characters When I build a review artifact with max diff size 100 And I serialize the artifact to rich format Then the rich text should contain "[yellow]" And the rich text should contain "truncated" Scenario: Rich format shows binary marker Given a change entry with operation "create" for path "bin.dat" in resource "RES001" And after content for "bin.dat" is "binary\x00data" When I build a review artifact from the changeset And I serialize the artifact to rich format Then the rich text should contain "[binary]" Scenario: Rich format colorizes diff lines Given a change entry with operation "modify" for path "c.py" in resource "RES001" And before content for "c.py" is "old\n" And after content for "c.py" is "new\n" When I build a review artifact from the changeset And I serialize the artifact to rich format Then the rich text should contain "[green]" And the rich text should contain "[red]" Scenario: Cross-resource delete and create are not renamed Given a delete entry for path "old/x.py" in resource "RES001" with before_hash "same" And a create entry for path "new/x.py" in resource "RES002" with after_hash "same" When I build a review artifact from the changeset Then the review artifact should have 2 groups And entry 0 in group 0 should not be a rename And entry 0 in group 1 should not be a rename Scenario: Multiple renames consume creates only once Given a delete entry for path "old/a.py" in resource "RES001" with before_hash "h1" And a delete entry for path "old/b.py" in resource "RES001" with before_hash "h1" And a create entry for path "new/a.py" in resource "RES001" with after_hash "h1" When I build a review artifact from the changeset Then group 0 should have 2 entries Scenario: Rename entry truncated when budget exhausted Given a change entry with operation "create" for path "a.py" in resource "RES001" And after content for "a.py" is a string of 1000 characters And a delete entry for path "old/r.py" in resource "RES001" with before_hash "rh1" And a create entry for path "new/r.py" in resource "RES001" with after_hash "rh1" When I build a review artifact with max diff size 50 Then the review artifact should be truncated And entry 1 in group 0 should be a rename And entry 1 in group 0 should be truncated And the diff text in entry 1 of group 0 should contain "[truncated]" Scenario: Plain serialization shows binary marker Given a change entry with operation "create" for path "img.png" in resource "RES001" And after content for "img.png" is "png\x00header" When I build a review artifact from the changeset And I serialize the artifact to plain text Then the plain text should contain "[binary file]"