From f56b2378db0cff072acf11745a8b3caa6785e2a2 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 14:31:58 +0700 Subject: [PATCH] spec(claw): suggest.t27 -- one-click formatter suggestions, COMMENT only, RIGHT side only (Closes #10285) The law behind the pr-files and pr-suggestions rows of github_surface: the review event is always COMMENT, comments anchor on the RIGHT side inside one diff hunk, at most 20 per review, and an unreadable hunk withholds its file. Diff ranges are read from the @@ headers of pr-files. 16 tests, 0 vacuous on the Railway lab; a mutant that lets event_for return its input fails the suite. Co-Authored-By: Claude Opus 5.5 --- specs/claw/suggest.t27 | 553 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 553 insertions(+) create mode 100644 specs/claw/suggest.t27 diff --git a/specs/claw/suggest.t27 b/specs/claw/suggest.t27 new file mode 100644 index 0000000000..1a9bd8c5a4 --- /dev/null +++ b/specs/claw/suggest.t27 @@ -0,0 +1,553 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/suggest.t27 -- one-click formatter suggestions on a pull request: the law behind the +; github_surface rows pr-files (pull_requests read, GET /pulls/{n}/files) and pr-suggestions +; (pull_requests write, POST /pulls/{n}/reviews). Epic #9271. +; Owner, 2026-10-18, translated: the App holds write on about 40 permissions; connect the GitHub +; API where it gives .t27 value. Owner rule, standing: agents never approve a pull request. +; -- +; The job. When a PR changes a .t27 file that `t27c fmt` would rewrite, TRI CLAW posts ONE review +; whose comments are ```suggestion blocks; the author accepts each in one click. Everything that +; decides WHETHER and WHERE a comment may be posted is in this file; the plumbing (fetch the file +; list, run the formatter, diff, POST) is a thin workflow around it. +; -- +; The five laws, each pinned by a test: +; 1. EVENT is COMMENT and nothing else. APPROVE, REQUEST_CHANGES, PENDING, an empty string, a +; wrong-case string: event_for returns "" and the caller posts nothing. A review that cannot be +; COMMENT does not exist. +; 2. RIGHT side only. A suggestion edits the PR head, so it anchors on the head file's lines; the +; LEFT side is the base and a suggestion there would propose to edit code the PR deleted. +; 3. Lines outside the diff are skipped. GitHub rejects a review comment whose line is not in the +; PR diff, and one rejected comment rejects the whole review; a multi-line range must also sit +; inside ONE diff hunk. The diff ranges come from the `@@ -a,b +c,d @@` headers of the `patch` +; field of pr-files (header_new_range). +; 4. Capped. At most MAX_COMMENTS per review and MAX_SPAN lines per suggestion; a bigger rewrite +; is not a one-click change and is left to `t27c fmt` on the author's side. +; 5. Fail closed on unknown hunks. A hunk the planner cannot interpret (unknown kind, line 0, +; inconsistent count, overlapping another hunk of the same file) poisons its file: nothing is +; posted for that file, because a hunk list we cannot read is not one we may anchor on. An +; unknown fact never becomes a comment. +; -- +; Hunk model: a formatter hunk says "head-file lines [start, start+count) become the replacement". +; K_REPLACE and K_DELETE cover count >= 1 original lines (a delete suggestion has an empty body). +; K_INSERT has count 0 (new lines go BEFORE line start); GitHub cannot anchor on nothing, so the +; comment anchors on line `start` itself and the replacement text must repeat that line -- the +; plumbing's job; widened() says which hunks need it. +; The honest limit: this file decides placement, not text. It does not compute the formatter's +; replacement lines, escape them into JSON, or call GitHub; and an adjacent insert/replace pair +; (insert before line 5 beside a replace ending at line 5) reads as overlapping and poisons the +; file -- fail closed costs a missed suggestion, never a wrong one. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawSuggest; + +/// The only review event this module ever lets through. +pub const EVENT : str = "COMMENT"; +/// The only side a suggestion anchors on. +pub const SIDE : str = "RIGHT"; +/// The Markdown fence that opens a suggestion block; it closes with a bare fence line. +pub const FENCE : str = "```suggestion"; + +/// Comments per review and lines per suggestion (law 4). +pub const MAX_COMMENTS : u32 = 20; +pub const MAX_SPAN : u32 = 40; + +/// Hunk kinds. +pub const K_REPLACE : u8 = 0; +pub const K_DELETE : u8 = 1; +pub const K_INSERT : u8 = 2; + +/// Per-hunk verdicts. V_UNKNOWN is far from the real ones: a partial read cannot mistake it +/// for a verdict. +pub const V_POST : u8 = 0; +pub const V_OUTSIDE : u8 = 1; +pub const V_CAP : u8 = 2; +pub const V_TOO_BIG : u8 = 3; +pub const V_UNKNOWN : u8 = 255; + +/// Byte equality for two counted strings (the idiom of specs/claw/gates.t27). +pub fn str_eq(a : str, alen : usize, b : str, blen : usize) -> bool { + if (alen != blen) { return false; } + var diff : usize = 0; + var i : usize = 0; + while (i < alen) { + if (a[i] != b[i]) { diff += 1; } + i += 1; + } + return diff == 0; +} + +/// Law 1. The event a review may carry: "COMMENT" when COMMENT was asked for, "" otherwise. +/// "" means the caller posts nothing. Nothing else can be returned, so no input reaches APPROVE. +pub fn event_for(requested : str, requested_len : usize) -> str { + if (str_eq(requested, requested_len, EVENT, 7)) { return EVENT; } + return ""; +} + +/// Law 2. The side a comment may use for `side`: RIGHT when RIGHT was asked for, "" otherwise. +pub fn side_for(requested : str, requested_len : usize) -> str { + if (str_eq(requested, requested_len, SIDE, 5)) { return SIDE; } + return ""; +} + +/// Whether a path names a .t27 file (only those are formatted, so only those get suggestions). +pub fn path_is_t27(path : str, n : usize) -> bool { + if (n < 5) { return false; } + if (path[n - 4] != '.') { return false; } + if (path[n - 3] != 't') { return false; } + if (path[n - 2] != '2') { return false; } + if (path[n - 1] != '7') { return false; } + return true; +} + +fn byte_at(t : []const u8, n : usize, i : usize) -> u8 { + if (i >= n) { return 0; } + return t[i]; +} + +fn is_digit(b : u8) -> bool { + return b >= '0' and b <= '9'; +} + +/// End index of the digit run that starts at p (p itself when there is none). +fn num_end(t : []const u8, n : usize, p : usize) -> usize { + var e : usize = p; + while (e < n) { + if (is_digit(t[e]) == false) { return e; } + e += 1; + } + return e; +} + +fn num_val(t : []const u8, p : usize, e : usize) -> u32 { + var v : u32 = 0; + var i : usize = p; + while (i < e) { + v = v * 10 + ((t[i] - '0')) as u32; + i += 1; + } + return v; +} + +/// Law 3 input. Parse the head side of a unified-diff hunk header, `@@ -a,b +c,d @@ ...`, +/// into out[0] = c (first head line) and out[1] = d (head line count; 1 when ",d" is omitted). +/// false on anything malformed or on a number longer than 7 digits: no range, no comments. +pub fn header_new_range(t : []const u8, n : usize, out : []u32) -> bool { + if (byte_at(t, n, 0) != '@') { return false; } + if (byte_at(t, n, 1) != '@') { return false; } + if (byte_at(t, n, 2) != ' ') { return false; } + if (byte_at(t, n, 3) != '-') { return false; } + var p : usize = 4; + var e : usize = num_end(t, n, p); + if (e == p) { return false; } + p = e; + if (byte_at(t, n, p) == ',') { + p += 1; + e = num_end(t, n, p); + if (e == p) { return false; } + p = e; + } + if (byte_at(t, n, p) != ' ') { return false; } + if (byte_at(t, n, p + 1) != '+') { return false; } + p += 2; + e = num_end(t, n, p); + if (e == p) { return false; } + if (e - p > 7) { return false; } + const start = num_val(t, p, e); + p = e; + var len : u32 = 1; + if (byte_at(t, n, p) == ',') { + p += 1; + e = num_end(t, n, p); + if (e == p) { return false; } + if (e - p > 7) { return false; } + len = num_val(t, p, e); + p = e; + } + if (byte_at(t, n, p) != ' ') { return false; } + if (byte_at(t, n, p + 1) != '@') { return false; } + if (byte_at(t, n, p + 2) != '@') { return false; } + out[0] = start; + out[1] = len; + return true; +} + +/// Whether the hunk shape is one the planner can interpret: known kind, start >= 1, count +/// consistent with kind (INSERT has none, REPLACE/DELETE have at least one). +pub fn shape_ok(kind : u8, start : u32, count : u32) -> bool { + if (start < 1) { return false; } + if (kind == K_INSERT) { return count == 0; } + if (kind == K_REPLACE or kind == K_DELETE) { return count >= 1; } + return false; +} + +/// First head line the comment anchors on. Only meaningful when shape_ok. +pub fn first_line(start : u32) -> u32 { + return start; +} + +/// Last head line the comment anchors on: an INSERT anchors on its own start line. +pub fn last_line(start : u32, count : u32) -> u32 { + if (count == 0) { return start; } + return start + count - 1; +} + +/// The review-comment `line` field (the last line of the range). +pub fn comment_line(start : u32, count : u32) -> u32 { + return last_line(start, count); +} + +/// The review-comment `start_line` field; 0 means "omit it" -- a one-line comment has none, +/// and GitHub requires start_line < line when present. +pub fn comment_start_line(start : u32, count : u32) -> u32 { + if (last_line(start, count) > start) { return start; } + return 0; +} + +/// Whether the replacement text must repeat the anchor line (an INSERT, which has no line). +pub fn widened(count : u32) -> bool { + return count == 0; +} + +/// Law 3. Whether [first, last] of `file` lies inside ONE head-side diff range. The ranges are +/// parallel arrays: dfile[k], dstart[k], dlen[k] for k < nd. An empty range (dlen 0, a pure +/// deletion hunk) covers no head line. +pub fn in_one_range(file : u32, first : u32, last : u32, dfile : []const u32, dstart : []const u32, dlen : []const u32, nd : usize) -> bool { + var k : usize = 0; + while (k < nd) { + if (dfile[k] == file and dlen[k] >= 1) { + if (dstart[k] <= first and last <= dstart[k] + dlen[k] - 1) { return true; } + } + k += 1; + } + return false; +} + +/// Whether hunk i of nh is unreadable: bad shape, or its anchor range overlaps another hunk of +/// the same file (two suggestions on one line cannot both be accepted). +pub fn hunk_unknown(i : usize, nh : usize, hfile : []const u32, hstart : []const u32, hcount : []const u32, hkind : []const u8) -> bool { + if (shape_ok(hkind[i], hstart[i], hcount[i]) == false) { return true; } + const f1 = first_line(hstart[i]); + const l1 = last_line(hstart[i], hcount[i]); + var j : usize = 0; + while (j < nh) { + if (j != i and hfile[j] == hfile[i]) { + if (shape_ok(hkind[j], hstart[j], hcount[j])) { + const f2 = first_line(hstart[j]); + const l2 = last_line(hstart[j], hcount[j]); + if (f1 <= l2 and f2 <= l1) { return true; } + } + } + j += 1; + } + return false; +} + +/// Law 5. Whether any hunk of `file` is unknown: the whole file is withheld. +pub fn file_poisoned(file : u32, nh : usize, hfile : []const u32, hstart : []const u32, hcount : []const u32, hkind : []const u8) -> bool { + var i : usize = 0; + while (i < nh) { + if (hfile[i] == file) { + if (hunk_unknown(i, nh, hfile, hstart, hcount, hkind)) { return true; } + } + i += 1; + } + return false; +} + +/// The planner. Fills out[i] with the verdict of hunk i (V_POST, V_OUTSIDE, V_CAP, V_TOO_BIG or +/// V_UNKNOWN) and returns how many hunks get a comment. Hunks post in input order until +/// MAX_COMMENTS; an unknown hunk (law 5) takes its whole file down with it. +pub fn plan(nh : usize, hfile : []const u32, hstart : []const u32, hcount : []const u32, hkind : []const u8, nd : usize, dfile : []const u32, dstart : []const u32, dlen : []const u32, out : []u8) -> u32 { + var posted : u32 = 0; + var i : usize = 0; + while (i < nh) { + out[i] = V_UNKNOWN; + if (file_poisoned(hfile[i], nh, hfile, hstart, hcount, hkind) == false) { + const f = first_line(hstart[i]); + const l = last_line(hstart[i], hcount[i]); + if (l - f + 1 > MAX_SPAN) { + out[i] = V_TOO_BIG; + } else if (in_one_range(hfile[i], f, l, dfile, dstart, dlen, nd) == false) { + out[i] = V_OUTSIDE; + } else if (posted >= MAX_COMMENTS) { + out[i] = V_CAP; + } else { + out[i] = V_POST; + posted += 1; + } + } + i += 1; + } + return posted; +} + +/// A review with no comment is never posted. +pub fn should_post(posted : u32) -> bool { + return posted >= 1; +} + +/// How many hunks carry verdict v. +pub fn count_verdict(v : u8, nh : usize, out : []const u8) -> u32 { + var n : u32 = 0; + var i : usize = 0; + while (i < nh) { + if (out[i] == v) { n += 1; } + i += 1; + } + return n; +} + +test event_is_never_approve { + // Runtime loop over every event name a caller could pass; none may come back as APPROVE + // (or as anything but COMMENT or the refusal). + var names : [8]str = ["APPROVE", "approve", "REQUEST_CHANGES", "PENDING", "", "COMMENT ", "comment", "DISMISS"]; + for i in 0..8 { + const e = event_for(names[i], names[i].len); + assert str_eq(e, e.len, "APPROVE", 7) == false; + assert str_eq(e, e.len, "REQUEST_CHANGES", 15) == false; + assert e.len == 0; + } + const ok = event_for("COMMENT", 7); + assert str_eq(ok, ok.len, "COMMENT", 7) == true; + assert str_eq(EVENT, EVENT.len, "APPROVE", 7) == false; +} + +test side_is_right_only { + var names : [4]str = ["LEFT", "left", "", "RIGHT "]; + for i in 0..4 { + const s = side_for(names[i], names[i].len); + assert s.len == 0; + } + const r = side_for("RIGHT", 5); + assert str_eq(r, r.len, "RIGHT", 5) == true; + assert str_eq(SIDE, SIDE.len, "LEFT", 4) == false; +} + +test lengths_pinned_to_the_bytes { + // event_for and side_for pass these lengths as literals; a loop keeps the asserts at runtime. + var names : [3]str = [EVENT, SIDE, FENCE]; + var lens : [3]usize = [7, 5, 13]; + for i in 0..3 { assert names[i].len == lens[i]; } +} + +test only_t27_paths_are_formatted { + var yes : [3]str = ["specs/claw/suggest.t27", "a.t27", "x/y/z.t27"]; + var no : [6]str = ["a.rs", "t27", ".t27", "specs/a.t27x", "specs/a.T27", "gen/a.zig"]; + for i in 0..3 { assert path_is_t27(yes[i], yes[i].len) == true; } + for i in 0..6 { assert path_is_t27(no[i], no[i].len) == false; } +} + +test header_parse_reads_the_head_side { + var out : [2]u32 = [0, 0]; + var h1 : str = "@@ -1,4 +1,5 @@ module X"; + assert header_new_range(h1, h1.len, out) == true; + assert out[0] == 1; + assert out[1] == 5; + var h2 : str = "@@ -10 +10 @@"; + assert header_new_range(h2, h2.len, out) == true; + assert out[0] == 10; + assert out[1] == 1; + var h3 : str = "@@ -0,0 +1,3 @@"; + assert header_new_range(h3, h3.len, out) == true; + assert out[0] == 1; + assert out[1] == 3; + var h4 : str = "@@ -5,2 +4,0 @@"; + assert header_new_range(h4, h4.len, out) == true; + assert out[0] == 4; + assert out[1] == 0; + var h5 : str = "@@ -120,33 +2048,17 @@ fn f()"; + assert header_new_range(h5, h5.len, out) == true; + assert out[0] == 2048; + assert out[1] == 17; +} + +test header_parse_fails_closed { + var out : [2]u32 = [7, 7]; + var bad : [9]str = ["", "@@", "@ -1 +1 @@", "@@ -1,2 +x @@", "@@ -a +1 @@", "@@ -1 +1", "@@ -1,2 +1,@@", "@@ -1 +12345678 @@", "diff --git a b"]; + for i in 0..9 { + assert header_new_range(bad[i], bad[i].len, out) == false; + // A failed parse writes nothing. + assert out[0] == 7; + assert out[1] == 7; + } +} + +test anchors_follow_the_hunk_kind { + // REPLACE of lines 10..12. + assert comment_line(10, 3) == 12; + assert comment_start_line(10, 3) == 10; + assert widened(3) == false; + // One-line REPLACE: single `line`, no start_line. + assert comment_line(10, 1) == 10; + assert comment_start_line(10, 1) == 0; + // INSERT before line 10 anchors on line 10 and must repeat it. + assert comment_line(10, 0) == 10; + assert comment_start_line(10, 0) == 0; + assert widened(0) == true; + // start_line < line whenever it is present. + for c in 0..8 { + const n = (c) as u32; + const s = comment_start_line(20, n); + if (s != 0) { assert s < comment_line(20, n); } + } +} + +test shape_rejects_what_it_cannot_read { + assert shape_ok(K_REPLACE, 5, 2) == true; + assert shape_ok(K_DELETE, 5, 1) == true; + assert shape_ok(K_INSERT, 5, 0) == true; + assert shape_ok(K_REPLACE, 0, 2) == false; + assert shape_ok(K_REPLACE, 5, 0) == false; + assert shape_ok(K_DELETE, 5, 0) == false; + assert shape_ok(K_INSERT, 5, 1) == false; + var k : u8 = 3; + for n in 0..4 { + assert shape_ok(k, 5, (n) as u32) == false; + k += 40; + } +} + +test planner_poisoned_file_posts_nothing_other_file_unaffected { + // File 1: head diff ranges [10..19] and [40..44]. File 2: [1..3]. + var df : [3]u32 = [1, 1, 2]; + var ds : [3]u32 = [10, 40, 1]; + var dl : [3]u32 = [10, 5, 3]; + // Hunks: 0 inside range 1; 1 straddles the gap (18..21); 2 inside range 2; 3 outside all; + // 4 an INSERT before line 12 (anchor 12, inside); 5 on file 2 inside; 6 on file 2 outside. + var hf : [7]u32 = [1, 1, 1, 1, 1, 2, 2]; + var hs : [7]u32 = [11, 18, 41, 30, 12, 2, 9]; + var hc : [7]u32 = [2, 4, 1, 1, 0, 1, 1]; + var hk : [7]u8 = [K_REPLACE, K_REPLACE, K_DELETE, K_REPLACE, K_INSERT, K_REPLACE, K_REPLACE]; + var out : [7]u8 = [9, 9, 9, 9, 9, 9, 9]; + // Hunk 4 (anchor 12) overlaps hunk 0 (11..12): that poisons file 1 entirely. + const posted = plan(7, hf, hs, hc, hk, 3, df, ds, dl, out); + for i in 0..5 { assert out[i] == V_UNKNOWN; } + assert out[5] == V_POST; + assert out[6] == V_OUTSIDE; + assert posted == 1; +} + +test planner_clean_file_classifies_each_hunk { + var df : [2]u32 = [1, 1]; + var ds : [2]u32 = [10, 40]; + var dl : [2]u32 = [10, 5]; + // 0 inside; 1 straddles the gap 18..21 (two diff ranges: not ONE hunk); 2 delete inside; + // 3 outside; 4 insert before 15 (anchor 15 inside). + var hf : [5]u32 = [1, 1, 1, 1, 1]; + var hs : [5]u32 = [11, 18, 41, 30, 15]; + var hc : [5]u32 = [2, 4, 1, 1, 0]; + var hk : [5]u8 = [K_REPLACE, K_REPLACE, K_DELETE, K_REPLACE, K_INSERT]; + var out : [5]u8 = [9, 9, 9, 9, 9]; + const posted = plan(5, hf, hs, hc, hk, 2, df, ds, dl, out); + assert out[0] == V_POST; + assert out[1] == V_OUTSIDE; + assert out[2] == V_POST; + assert out[3] == V_OUTSIDE; + assert out[4] == V_POST; + assert posted == 3; + assert count_verdict(V_POST, 5, out) == 3; + assert count_verdict(V_OUTSIDE, 5, out) == 2; + assert count_verdict(V_UNKNOWN, 5, out) == 0; + assert should_post(posted) == true; +} + +test a_range_across_two_diff_hunks_is_not_one_hunk { + // Diff ranges [10..19] and [20..29] touch but are two hunks of the diff: a multi-line + // comment across them is rejected by GitHub, so it is skipped. + var df : [2]u32 = [1, 1]; + var ds : [2]u32 = [10, 20]; + var dl : [2]u32 = [10, 10]; + assert in_one_range(1, 18, 21, df, ds, dl, 2) == false; + assert in_one_range(1, 18, 19, df, ds, dl, 2) == true; + assert in_one_range(1, 20, 21, df, ds, dl, 2) == true; + // Same line numbers on another file never match. + assert in_one_range(2, 18, 19, df, ds, dl, 2) == false; + // A deletion-only diff range (len 0) covers nothing. + var zl : [2]u32 = [0, 0]; + assert in_one_range(1, 10, 10, df, ds, zl, 2) == false; +} + +test the_cap_stops_the_twenty_first_comment { + // 25 one-line hunks, lines 1,3,5,...,49 (no overlap), diff covers lines 1..60. + var df : [1]u32 = [1]; + var ds : [1]u32 = [1]; + var dl : [1]u32 = [60]; + var hf : [25]u32 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; + var hs : [25]u32 = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49]; + var hc : [25]u32 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; + var hk : [25]u8 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + var out : [25]u8 = [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9]; + const posted = plan(25, hf, hs, hc, hk, 1, df, ds, dl, out); + assert posted == MAX_COMMENTS; + assert count_verdict(V_POST, 25, out) == 20; + assert count_verdict(V_CAP, 25, out) == 5; + for i in 0..20 { assert out[i] == V_POST; } + for i in 20..25 { assert out[i] == V_CAP; } +} + +test a_rewrite_over_the_span_cap_is_left_to_the_author { + var df : [1]u32 = [1]; + var ds : [1]u32 = [1]; + var dl : [1]u32 = [200]; + var hf : [2]u32 = [1, 1]; + var hs : [2]u32 = [10, 100]; + var hc : [2]u32 = [41, 40]; + var hk : [2]u8 = [K_REPLACE, K_REPLACE]; + var out : [2]u8 = [9, 9]; + const posted = plan(2, hf, hs, hc, hk, 1, df, ds, dl, out); + assert out[0] == V_TOO_BIG; + assert out[1] == V_POST; + assert posted == 1; +} + +test an_unknown_hunk_poisons_only_its_own_file { + var df : [2]u32 = [1, 2]; + var ds : [2]u32 = [1, 1]; + var dl : [2]u32 = [50, 50]; + // File 1: a good hunk and a hunk of unknown kind 7. File 2: one good hunk. + var hf : [3]u32 = [1, 1, 2]; + var hs : [3]u32 = [5, 20, 5]; + var hc : [3]u32 = [1, 1, 1]; + var hk : [3]u8 = [K_REPLACE, 7, K_REPLACE]; + var out : [3]u8 = [9, 9, 9]; + const posted = plan(3, hf, hs, hc, hk, 2, df, ds, dl, out); + assert out[0] == V_UNKNOWN; + assert out[1] == V_UNKNOWN; + assert out[2] == V_POST; + assert posted == 1; + assert file_poisoned(1, 3, hf, hs, hc, hk) == true; + assert file_poisoned(2, 3, hf, hs, hc, hk) == false; +} + +test overlapping_hunks_are_unknown { + var hf : [2]u32 = [1, 1]; + var hs : [2]u32 = [5, 6]; + var hc : [2]u32 = [3, 1]; + var hk : [2]u8 = [K_REPLACE, K_REPLACE]; + assert hunk_unknown(0, 2, hf, hs, hc, hk) == true; + assert hunk_unknown(1, 2, hf, hs, hc, hk) == true; + // The same lines on different files do not collide. + var hf2 : [2]u32 = [1, 2]; + assert hunk_unknown(0, 2, hf2, hs, hc, hk) == false; + assert hunk_unknown(1, 2, hf2, hs, hc, hk) == false; + // Touching but disjoint: 5..7 and 8..8. + var hs3 : [2]u32 = [5, 8]; + assert hunk_unknown(0, 2, hf, hs3, hc, hk) == false; +} + +test no_hunks_and_no_ranges_post_nothing { + var df : [1]u32 = [1]; + var ds : [1]u32 = [1]; + var dl : [1]u32 = [10]; + var hf : [1]u32 = [1]; + var hs : [1]u32 = [5]; + var hc : [1]u32 = [1]; + var hk : [1]u8 = [K_REPLACE]; + var out : [1]u8 = [9]; + // No hunks at all. + assert plan(0, hf, hs, hc, hk, 1, df, ds, dl, out) == 0; + assert should_post(0) == false; + // A hunk but no diff ranges (pr-files gave no patch, e.g. a binary or oversized file). + assert plan(1, hf, hs, hc, hk, 0, df, ds, dl, out) == 0; + assert out[0] == V_OUTSIDE; +}