~bigbes/sr-ht-spec

ref: a4d8cc52e917cf1db07c876e8b36654dcf28741b sr-ht-spec/core/comment_test.go -rw-r--r-- 6.6 KiB
a4d8cc52 — Eugene Blikh authn: remove the local agent-token plane 9 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package core

import (
	"errors"
	"testing"
)

// blocks builds a revision out of "heading/path|hash" shorthand, numbering each
// block within its heading path exactly as AnchorBlocks does.
func blocks(specs ...string) []AnchorBlock {
	hashes := make([]string, len(specs))
	paths := make([][]string, len(specs))
	for i, s := range specs {
		var path []string
		hash := s
		for j := len(s) - 1; j >= 0; j-- {
			if s[j] == '|' {
				hash = s[j+1:]
				if head := s[:j]; head != "" {
					path = split(head, '/')
				}
				break
			}
		}
		hashes[i], paths[i] = hash, path
	}
	return AnchorBlocks(hashes, paths)
}

func split(s string, sep byte) []string {
	var out []string
	start := 0
	for i := range len(s) {
		if s[i] == sep {
			out = append(out, s[start:i])
			start = i + 1
		}
	}
	return append(out, s[start:])
}

func anchor(headings []string, index int, hash string) CommentAnchor {
	return CommentAnchor{DocID: "SPEC-0007", HeadingPath: headings, Index: index, BlockHash: hash, Side: SideNew}
}

// Content is the strongest evidence an anchor has. A paragraph that reflowed,
// or that a dozen insertions above pushed down the document, is still the same
// paragraph, and the comment on it must not be disturbed.
func TestAnchorFollowsContentWhereverItMoves(t *testing.T) {
	rev := blocks("Storage|aaa", "Storage|bbb", "Storage|ccc")
	a := anchor([]string{"Storage"}, 2, "ccc")

	// Two blocks inserted above: the commented block is now at index 4.
	moved := blocks("Storage|new1", "Storage|new2", "Storage|aaa", "Storage|bbb", "Storage|ccc")

	for name, tc := range map[string]struct {
		rev  []AnchorBlock
		want int
	}{
		"unchanged":      {rev, 2},
		"shifted by two": {moved, 4},
	} {
		got, state := ResolveAnchor(a, tc.rev)
		if got != tc.want || state != AnchorExact {
			t.Errorf("%s: ResolveAnchor = (%d, %s), want (%d, %s)", name, got, state, tc.want, AnchorExact)
		}
	}
}

// Renaming a section must not orphan the comments inside it. This is why
// HeadingPath is kept out of the block hash: the content is untouched, so the
// hash still matches and the comment survives a rename it had nothing to do
// with.
func TestSectionRenameDoesNotOrphanItsComments(t *testing.T) {
	a := anchor([]string{"Storage"}, 1, "bbb")
	renamed := blocks("Storage model|aaa", "Storage model|bbb")

	got, state := ResolveAnchor(a, renamed)
	if got != 1 || state != AnchorExact {
		t.Errorf("ResolveAnchor = (%d, %s), want (1, %s)", got, state, AnchorExact)
	}
}

// The fallback: the text was edited, so no hash matches, but a block still sits
// at that spot under those headings. The comment is shown against it and marked
// edited, because the critique may no longer fit the words.
func TestEditedBlockKeepsTheCommentAndSaysSo(t *testing.T) {
	a := anchor([]string{"Storage"}, 1, "bbb")
	edited := blocks("Storage|aaa", "Storage|bbb-rewritten", "Storage|ccc")

	got, state := ResolveAnchor(a, edited)
	if got != 1 || state != AnchorEdited {
		t.Errorf("ResolveAnchor = (%d, %s), want (1, %s)", got, state, AnchorEdited)
	}
}

// The index is within the heading path, not the document. An edit in an earlier
// section changes every document-global position below it, and the fallback
// exists precisely for blocks whose content changed — so a global index would
// fail exactly when it is needed.
func TestIndexIsRelativeToTheSectionNotTheDocument(t *testing.T) {
	// The comment is on the second block of "Storage": document index 3.
	a := anchor([]string{"Storage"}, 1, "bbb")
	// A block is added to the earlier section and the commented block is
	// rewritten, so only the positional fallback can fire.
	rev := blocks("Intro|i1", "Intro|i2", "Intro|i3", "Storage|aaa", "Storage|bbb-rewritten")

	got, state := ResolveAnchor(a, rev)
	if state != AnchorEdited {
		t.Fatalf("state = %s, want %s (a section-relative index survives an insertion above)", state, AnchorEdited)
	}
	if got != 4 {
		t.Errorf("ResolveAnchor = %d, want 4 (the second Storage block, now at document index 4)", got)
	}
}

// Nothing matched. The comment is kept and reported as outdated rather than
// relocated to a best guess: a comment on the wrong paragraph is worse than one
// that admits it lost its place, because the reader cannot tell it is wrong.
func TestNothingMatchedIsOutdatedNotRelocated(t *testing.T) {
	a := anchor([]string{"Storage"}, 4, "bbb")
	gone := blocks("Intro|i1", "Rationale|r1")

	got, state := ResolveAnchor(a, gone)
	if got != -1 || state != AnchorOutdated {
		t.Errorf("ResolveAnchor = (%d, %s), want (-1, %s)", got, state, AnchorOutdated)
	}
}

// A document repeats itself — "TBD" appears verbatim under half the headings —
// so a hash match alone does not identify a block. The comment's own section
// wins outright, however far the block moved inside it; otherwise which
// duplicate a comment landed on would be decided by document order.
func TestDuplicateContentIsDisambiguatedByItsSection(t *testing.T) {
	rev := blocks("Intro|TBD", "Rationale|TBD", "Storage|s1", "Storage|s2", "Storage|s3", "Storage|TBD")

	got, state := ResolveAnchor(anchor([]string{"Storage"}, 3, "TBD"), rev)
	if got != 5 || state != AnchorExact {
		t.Errorf("ResolveAnchor = (%d, %s), want (5, %s) — Storage's own TBD", got, state, AnchorExact)
	}

	// With no section of its own to prefer, the nearest index decides, and it
	// does so deterministically rather than by walk order.
	got, _ = ResolveAnchor(anchor([]string{"Gone"}, 0, "TBD"), rev)
	if got != 0 {
		t.Errorf("ResolveAnchor = %d, want 0 (nearest index when no section matches)", got)
	}
}

// A comment on a block with no enclosing heading — the document preamble — is
// an ordinary case, not a missing value.
func TestPreambleBlocksAnchorLikeAnyOther(t *testing.T) {
	rev := blocks("|p1", "|p2", "Storage|s1")

	got, state := ResolveAnchor(anchor(nil, 1, "p2"), rev)
	if got != 1 || state != AnchorExact {
		t.Errorf("ResolveAnchor = (%d, %s), want (1, %s)", got, state, AnchorExact)
	}
}

// Heading paths are compared whole. Two sections whose names concatenate to the
// same string are different sections, and a comment must not cross between them.
func TestHeadingPathsDoNotCollideByConcatenation(t *testing.T) {
	rev := blocks("A/BC|x", "AB/C|y")

	got, _ := ResolveAnchor(anchor([]string{"AB", "C"}, 0, "y"), rev)
	if got != 1 {
		t.Errorf("ResolveAnchor = %d, want 1; [A BC] and [AB C] are different sections", got)
	}
}

func TestParseCommentSide(t *testing.T) {
	for _, s := range []string{"new", "old"} {
		if _, err := ParseCommentSide(s); err != nil {
			t.Errorf("ParseCommentSide(%q): %v", s, err)
		}
	}
	if _, err := ParseCommentSide("both"); !errors.Is(err, ErrInvalidCommentSide) {
		t.Errorf("ParseCommentSide(both) error = %v, want ErrInvalidCommentSide", err)
	}
}