~bigbes/sr-ht-spec

ref: 61515a575dc7e931829c05eed0cd3c3a441df753 sr-ht-spec/web/diff_internal_test.go -rw-r--r-- 12.6 KiB
61515a57 — Eugene Blikh chore(beads): file spec-ejq.2, CI publish blocked on missing apk-ci-s3 secret 24 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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
package web

import (
	"strings"
	"testing"

	"sourcecraft.dev/bigbes/sr-ht-spec/core"
	"sourcecraft.dev/bigbes/sr-ht-spec/service"
)

// render is renderDocDiff for a document nobody has commented on, which is what
// the presentation tests are about.
func render(oldSrc, newSrc []byte) diffView {
	return renderDocDiff(docDiff{DocID: "SPEC-0007", Path: "specs/0007-storage.md", Base: oldSrc, Proposed: newSrc})
}

// blockIDs pulls the rendered block ids out of a diff, in document order, so a
// test can compare two renders without depending on the digest itself.
func blockIDs(html string) []string {
	var ids []string
	for _, rest := range strings.Split(html, ` id="b-`)[1:] {
		ids = append(ids, "b-"+rest[:strings.IndexByte(rest, '"')])
	}
	return ids
}

// TestRenderDocDiffUnchanged proves a proposal that does not change a document
// is reported as unchanged rather than as an empty diff.
func TestRenderDocDiffUnchanged(t *testing.T) {
	src := []byte("# Title\n\nOne paragraph.\n")
	v := render(src, src)
	if !v.Unchanged {
		t.Fatalf("Unchanged = false, want true for identical input")
	}
	if v.HTML != "" {
		t.Errorf("HTML = %q, want empty for an unchanged document", v.HTML)
	}
}

// TestRenderDocDiffInlineWordChange proves a small edit renders inline with
// <del>/<ins> marks and not as two columns.
func TestRenderDocDiffInlineWordChange(t *testing.T) {
	old := []byte("# Title\n\nThe quick brown fox jumps over the lazy dog.\n")
	nw := []byte("# Title\n\nThe quick red fox jumps over the lazy dog.\n")
	v := render(old, nw)
	if v.Unchanged {
		t.Fatalf("Unchanged = true, want a change")
	}
	html := string(v.HTML)
	if !strings.Contains(html, "<del>brown</del>") {
		t.Errorf("missing inline deletion of 'brown'; got:\n%s", html)
	}
	if !strings.Contains(html, "<ins>red</ins>") {
		t.Errorf("missing inline insertion of 'red'; got:\n%s", html)
	}
	if strings.Contains(html, "ph-columns") {
		t.Errorf("a one-word edit rendered as two columns; got:\n%s", html)
	}
}

// TestRenderDocDiffTwoColumnBelowThreshold proves a block rewritten enough to
// fall below the similarity threshold renders as the two-column old/new view —
// the Phase 0 verdict's hard requirement.
func TestRenderDocDiffTwoColumnBelowThreshold(t *testing.T) {
	// A block rewritten to ~0.58 similarity: paired as a modify (above the 0.40
	// pairing floor) but shredded enough to fall below the 0.75 inline switch.
	old := []byte("# Title\n\nThe committee approved the annual budget after a long and " +
		"contentious debate that lasted well into the evening.\n")
	nw := []byte("# Title\n\nThe committee rejected the annual budget after a brief and " +
		"quiet discussion that ended early in the afternoon.\n")
	v := render(old, nw)
	html := string(v.HTML)
	if !strings.Contains(html, "ph-columns") {
		t.Fatalf("a wholesale rewrite did not render as two columns; got:\n%s", html)
	}
	if !strings.Contains(html, "ph-col ph-old") || !strings.Contains(html, "ph-col ph-new") {
		t.Errorf("two-column view missing an old or new column; got:\n%s", html)
	}
}

// TestRenderDocDiffEscapesContent proves document content is HTML-escaped: a
// document that contains markup cannot inject it into the review page.
func TestRenderDocDiffEscapesContent(t *testing.T) {
	old := []byte("# Title\n\nplain text here.\n")
	nw := []byte("# Title\n\nplain <script>alert(1)</script> text here.\n")
	v := render(old, nw)
	html := string(v.HTML)
	if strings.Contains(html, "<script>") {
		t.Fatalf("unescaped <script> reached the output; got:\n%s", html)
	}
	if !strings.Contains(html, "&lt;script&gt;") {
		t.Errorf("expected the escaped script tag in the output; got:\n%s", html)
	}
}

// TestRenderDocDiffEscapesUnchangedContent proves the context blocks the review
// now renders are escaped too — both the collapsed preview and the body.
func TestRenderDocDiffEscapesUnchangedContent(t *testing.T) {
	const untouched = "A <b>bold</b> claim nobody edited.\n"
	old := []byte("# Title\n\n" + untouched + "\nfirst.\n")
	nw := []byte("# Title\n\n" + untouched + "\nsecond.\n")
	html := string(render(old, nw).HTML)
	if strings.Contains(html, "<b>bold</b>") {
		t.Fatalf("unescaped markup from an unchanged block reached the output; got:\n%s", html)
	}
	if want := strings.Count(html, "&lt;b&gt;bold&lt;/b&gt;"); want != 2 {
		t.Errorf("escaped markup appears %d times, want 2 (the preview and the body); got:\n%s", want, html)
	}
}

// TestRenderDocDiffInsertAndDelete proves an added and a removed block are each
// shown whole, labelled.
func TestRenderDocDiffInsertAndDelete(t *testing.T) {
	old := []byte("# Title\n\nKept paragraph.\n\nDoomed paragraph.\n")
	nw := []byte("# Title\n\nKept paragraph.\n\nBrand new paragraph.\n")
	v := render(old, nw)
	html := string(v.HTML)
	if !strings.Contains(html, "ph-insert") {
		t.Errorf("missing an inserted block; got:\n%s", html)
	}
	if !strings.Contains(html, "ph-delete") {
		t.Errorf("missing a deleted block; got:\n%s", html)
	}
}

// TestUnchangedBlocksRenderAsSubordinateContext proves the reversal this phase
// makes: an unchanged block is on the page — every block of a proposed document
// must be commentable — but as collapsed context, so a changed block still reads
// as the thing that changed.
func TestUnchangedBlocksRenderAsSubordinateContext(t *testing.T) {
	old := []byte("# Title\n\nUntouched paragraph.\n\nOld wording here.\n")
	nw := []byte("# Title\n\nUntouched paragraph.\n\nNew wording here.\n")
	html := string(render(old, nw).HTML)

	if !strings.Contains(html, "Untouched paragraph.") {
		t.Fatalf("the unchanged block is not on the page; got:\n%s", html)
	}
	if !strings.Contains(html, `<details class="ph-block ph-context"`) {
		t.Errorf("the unchanged block is not rendered as collapsed context; got:\n%s", html)
	}
	// The changed block must stay plainly changed: its own class, its inline
	// marks, and no <details> wrapper hiding it behind a click.
	if !strings.Contains(html, `<div class="ph-block ph-modify"`) {
		t.Errorf("the changed block lost its own presentation; got:\n%s", html)
	}
	if !strings.Contains(html, "<ins>New</ins>") {
		t.Errorf("the changed block lost its inline marks; got:\n%s", html)
	}
}

// TestEveryRenderedBlockCarriesAnID proves a comment has something to point at
// and a link has something to scroll to, on changed and unchanged blocks alike.
func TestEveryRenderedBlockCarriesAnID(t *testing.T) {
	old := []byte("# Title\n\nUntouched paragraph.\n\nOld wording here.\n")
	nw := []byte("# Title\n\nUntouched paragraph.\n\nNew wording here.\n")
	html := string(render(old, nw).HTML)

	ids := blockIDs(html)
	// heading, untouched paragraph, modified paragraph.
	if len(ids) != 3 {
		t.Fatalf("rendered %d block ids, want 3; got:\n%s", len(ids), html)
	}
	seen := map[string]bool{}
	for _, id := range ids {
		if seen[id] {
			t.Errorf("duplicate block id %q; got:\n%s", id, html)
		}
		seen[id] = true
	}
}

// TestBlockIDIsStableAcrossARerender proves the id is a function of the block,
// not of anything the renderer accumulates as it goes.
func TestBlockIDIsStableAcrossARerender(t *testing.T) {
	old := []byte("# Title\n\nUntouched paragraph.\n\nOld wording here.\n")
	nw := []byte("# Title\n\nUntouched paragraph.\n\nNew wording here.\n")

	first := blockIDs(string(render(old, nw).HTML))
	second := blockIDs(string(render(old, nw).HTML))
	if len(first) == 0 || !equalStrings(first, second) {
		t.Fatalf("ids changed across a re-render:\n%v\n%v", first, second)
	}
}

// TestBlockIDSurvivesAnUnrelatedEdit proves an id names its own block: editing
// a different section of the document leaves it alone, so a link written down
// yesterday still lands on the paragraph it was written for.
func TestBlockIDSurvivesAnUnrelatedEdit(t *testing.T) {
	const base = "# Storage\n\nThe block under test.\n\n## Trade-offs\n\nSomething else entirely.\n"
	const first = base + "\nA tail paragraph.\n"
	const second = "# Storage\n\nThe block under test.\n\n## Trade-offs\n\nSomething else, rewritten.\n\nA tail paragraph.\n"

	before := blockIDs(string(render([]byte(base), []byte(first)).HTML))
	after := blockIDs(string(render([]byte(base), []byte(second)).HTML))
	if len(before) < 2 || len(after) < 2 {
		t.Fatalf("expected several blocks, got %d and %d", len(before), len(after))
	}
	// Index 1 is "The block under test." in both renders; the edit is two blocks
	// further down, under a different heading.
	if before[1] != after[1] {
		t.Errorf("an edit elsewhere in the document changed a block's id: %q vs %q", before[1], after[1])
	}
}

// TestThreadRendersOnItsOwnBlock proves an anchored comment is drawn against
// the block it anchors to and nowhere else.
func TestThreadRendersOnItsOwnBlock(t *testing.T) {
	const src = "# Storage\n\nThe commented paragraph.\n\nAn innocent bystander.\n"
	anchor, err := service.AnchorOf("SPEC-0007", []byte(src), 1, core.SideNew)
	if err != nil {
		t.Fatalf("AnchorOf: %v", err)
	}
	view := renderDocDiff(docDiff{
		DocID: "SPEC-0007", Path: "specs/a.md",
		Base:     []byte("# Storage\n\nThe commented paragraph.\n"),
		Proposed: []byte(src),
		Threads: []service.Thread{{
			Root:   service.Comment{ID: 4, Author: "bigbes", Body: "this is wrong"},
			Anchor: anchor, State: core.AnchorExact, Block: 1,
		}},
	})
	html := string(view.HTML)
	if len(view.Unplaced) != 0 {
		t.Fatalf("an anchored thread came back unplaced: %+v", view.Unplaced)
	}

	// The comment must sit inside the block it anchors to: the bystander block
	// opens after it, so the comment body has to appear before that boundary.
	comment := strings.Index(html, "this is wrong")
	bystander := strings.Index(html, "An innocent bystander")
	if comment < 0 {
		t.Fatalf("the comment is not on the page; got:\n%s", html)
	}
	if bystander > 0 && comment > bystander {
		t.Errorf("the comment rendered after the following block, not on its own; got:\n%s", html)
	}
}

// TestOutdatedThreadIsHandedBackNotAttached proves a comment whose anchor is
// lost is never drawn against some other block. It comes back for the page to
// show in its own area — visible, and attached to nothing.
func TestOutdatedThreadIsHandedBackNotAttached(t *testing.T) {
	view := renderDocDiff(docDiff{
		DocID: "SPEC-0007", Path: "specs/a.md",
		Base:     []byte("# Storage\n\nOne paragraph.\n"),
		Proposed: []byte("# Storage\n\nOne paragraph, edited.\n"),
		Threads: []service.Thread{{
			Root:  service.Comment{ID: 9, Author: "bigbes", Body: "orphaned critique"},
			State: core.AnchorOutdated, Block: -1,
		}},
	})
	if len(view.Unplaced) != 1 || view.Unplaced[0].Root.ID != 9 {
		t.Fatalf("Unplaced = %+v, want the outdated thread", view.Unplaced)
	}
	if strings.Contains(string(view.HTML), "orphaned critique") {
		t.Errorf("an outdated comment was rendered against a block; got:\n%s", view.HTML)
	}
}

// TestThreadOnAnUnrenderedBlockIsHandedBack proves the invariant the renderer
// is built on: a thread comes out either on its block or in Unplaced, never
// neither. An old-side anchor that still resolves against the base but whose
// block the diff no longer renders — the agent restored the deleted paragraph —
// is the case that would otherwise vanish.
func TestThreadOnAnUnrenderedBlockIsHandedBack(t *testing.T) {
	const src = "# Storage\n\nThe restored paragraph.\n"
	anchor, err := service.AnchorOf("SPEC-0007", []byte(src), 1, core.SideOld)
	if err != nil {
		t.Fatalf("AnchorOf: %v", err)
	}
	view := renderDocDiff(docDiff{
		DocID: "SPEC-0007", Path: "specs/a.md",
		Base:     []byte(src),
		Proposed: []byte(src + "\nAnd a new one.\n"),
		Threads: []service.Thread{{
			Root:   service.Comment{ID: 3, Body: "written when this was deleted"},
			Anchor: anchor, State: core.AnchorExact, Block: 1,
		}},
	})
	if len(view.Unplaced) != 1 {
		t.Fatalf("Unplaced = %+v, want the thread whose block is not rendered", view.Unplaced)
	}
	if strings.Contains(string(view.HTML), "written when this was deleted") {
		t.Errorf("the thread was rendered against some other block; got:\n%s", view.HTML)
	}
}

// TestUnchangedDocumentKeepsItsThreads proves nothing is lost in the degenerate
// case: a document whose diff renders no blocks at all still hands its threads
// back rather than swallowing them.
func TestUnchangedDocumentKeepsItsThreads(t *testing.T) {
	src := []byte("# Storage\n\nOne paragraph.\n")
	view := renderDocDiff(docDiff{
		DocID: "SPEC-0007", Path: "specs/a.md", Base: src, Proposed: src,
		Threads: []service.Thread{{Root: service.Comment{ID: 1, Body: "still here"}, State: core.AnchorExact, Block: 1}},
	})
	if !view.Unchanged {
		t.Fatalf("Unchanged = false for identical input")
	}
	if len(view.Unplaced) != 1 {
		t.Errorf("Unplaced = %+v, want the thread of an unrendered document", view.Unplaced)
	}
}

func equalStrings(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}