@cueloop/schema 0.1.0-alpha.6 → 0.1.0-alpha.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/anchor.test.ts +154 -39
- package/src/anchor.ts +161 -42
- package/src/diff.test.ts +42 -25
- package/src/diff.ts +112 -77
- package/src/feedback.test.ts +170 -40
- package/src/feedback.ts +124 -47
- package/src/fuzzy.test.ts +92 -0
- package/src/fuzzy.ts +101 -0
- package/src/herdr.test.ts +50 -0
- package/src/herdr.ts +54 -0
- package/src/index.ts +5 -0
- package/src/inline.test.ts +195 -0
- package/src/inline.ts +240 -0
- package/src/markdown.test.ts +76 -21
- package/src/markdown.ts +123 -72
- package/src/participants.test.ts +58 -0
- package/src/participants.ts +38 -0
- package/src/types.test.ts +30 -0
- package/src/types.ts +124 -9
- package/src/working-copy.test.ts +156 -0
- package/src/working-copy.ts +66 -0
package/src/diff.ts
CHANGED
|
@@ -1,52 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Line/token diffing and unified-diff serialization. The working copy
|
|
3
|
-
* serializes as ONE unified diff against the submitted revision
|
|
3
|
+
* serializes as ONE unified diff against the submitted revision.
|
|
4
4
|
* Pure algorithms, no IO.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export type
|
|
7
|
+
export type DiffOpKind = "ctx" | "del" | "add";
|
|
8
8
|
|
|
9
9
|
export interface DiffOp<T = string> {
|
|
10
|
-
|
|
10
|
+
kind: DiffOpKind;
|
|
11
11
|
/** Present for ctx and del. */
|
|
12
|
-
|
|
12
|
+
oldValue?: T;
|
|
13
13
|
/** Present for ctx and add. */
|
|
14
|
-
|
|
14
|
+
newValue?: T;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** LCS diff over arrays. O(n*m) - fine for plan-sized documents. */
|
|
18
|
-
export function lcsDiff<T>(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
export function lcsDiff<T>(
|
|
19
|
+
oldItems: T[],
|
|
20
|
+
newItems: T[],
|
|
21
|
+
equals: (oldItem: T, newItem: T) => boolean = (oldItem, newItem) => oldItem === newItem,
|
|
22
|
+
): DiffOp<T>[] {
|
|
23
|
+
const oldCount = oldItems.length;
|
|
24
|
+
const newCount = newItems.length;
|
|
25
|
+
const longestCommon: number[][] = Array.from({ length: oldCount + 1 }, () =>
|
|
26
|
+
Array.from({ length: newCount + 1 }, () => 0),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
for (let oldIndex = oldCount - 1; oldIndex >= 0; oldIndex--) {
|
|
30
|
+
for (let newIndex = newCount - 1; newIndex >= 0; newIndex--) {
|
|
31
|
+
longestCommon[oldIndex]![newIndex] = equals(oldItems[oldIndex]!, newItems[newIndex]!)
|
|
32
|
+
? longestCommon[oldIndex + 1]![newIndex + 1]! + 1
|
|
33
|
+
: Math.max(
|
|
34
|
+
longestCommon[oldIndex + 1]![newIndex]!,
|
|
35
|
+
longestCommon[oldIndex]![newIndex + 1]!,
|
|
36
|
+
);
|
|
25
37
|
}
|
|
26
38
|
}
|
|
27
|
-
const
|
|
28
|
-
let
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const ops: DiffOp<T>[] = [];
|
|
40
|
+
let oldIndex = 0;
|
|
41
|
+
let newIndex = 0;
|
|
42
|
+
|
|
43
|
+
while (oldIndex < oldCount && newIndex < newCount) {
|
|
44
|
+
if (equals(oldItems[oldIndex]!, newItems[newIndex]!)) {
|
|
45
|
+
ops.push({ kind: "ctx", oldValue: oldItems[oldIndex], newValue: newItems[newIndex] });
|
|
46
|
+
oldIndex++;
|
|
47
|
+
newIndex++;
|
|
48
|
+
} else if (longestCommon[oldIndex + 1]![newIndex]! >= longestCommon[oldIndex]![newIndex + 1]!) {
|
|
49
|
+
ops.push({ kind: "del", oldValue: oldItems[oldIndex] });
|
|
50
|
+
oldIndex++;
|
|
38
51
|
} else {
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
ops.push({ kind: "add", newValue: newItems[newIndex] });
|
|
53
|
+
newIndex++;
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
|
-
while (
|
|
44
|
-
while (
|
|
45
|
-
|
|
56
|
+
while (oldIndex < oldCount) ops.push({ kind: "del", oldValue: oldItems[oldIndex++] });
|
|
57
|
+
while (newIndex < newCount) ops.push({ kind: "add", newValue: newItems[newIndex++] });
|
|
58
|
+
|
|
59
|
+
return ops;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
export interface UnifiedLine {
|
|
49
|
-
|
|
63
|
+
kind: DiffOpKind | "hunk";
|
|
50
64
|
text: string;
|
|
51
65
|
}
|
|
52
66
|
|
|
@@ -56,58 +70,83 @@ export interface UnifiedLine {
|
|
|
56
70
|
*/
|
|
57
71
|
export function unifiedDiff(oldText: string, newText: string, context = 3): UnifiedLine[] | null {
|
|
58
72
|
const ops = lcsDiff(oldText.split("\n"), newText.split("\n"));
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
|
|
74
|
+
if (!ops.some((op) => op.kind !== "ctx")) return null;
|
|
75
|
+
let oldLineNumber = 1;
|
|
76
|
+
let newLineNumber = 1;
|
|
77
|
+
const rows = ops.map((op) => {
|
|
78
|
+
const row = {
|
|
79
|
+
kind: op.kind,
|
|
80
|
+
text: (op.kind === "add" ? op.newValue : op.oldValue) ?? "",
|
|
81
|
+
oldLine: op.kind !== "add" ? oldLineNumber : null,
|
|
82
|
+
newLine: op.kind !== "del" ? newLineNumber : null,
|
|
68
83
|
};
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
84
|
+
|
|
85
|
+
if (op.kind !== "add") oldLineNumber++;
|
|
86
|
+
if (op.kind !== "del") newLineNumber++;
|
|
87
|
+
|
|
88
|
+
return row;
|
|
72
89
|
});
|
|
73
|
-
const keep =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
const keep = Array.from({ length: rows.length }, () => false);
|
|
91
|
+
|
|
92
|
+
rows.forEach((row, rowIndex) => {
|
|
93
|
+
if (row.kind !== "ctx") {
|
|
94
|
+
for (
|
|
95
|
+
let keepIndex = Math.max(0, rowIndex - context);
|
|
96
|
+
keepIndex <= Math.min(rows.length - 1, rowIndex + context);
|
|
97
|
+
keepIndex++
|
|
98
|
+
) {
|
|
99
|
+
keep[keepIndex] = true;
|
|
100
|
+
}
|
|
77
101
|
}
|
|
78
102
|
});
|
|
79
103
|
const hunks: { rows: (typeof rows)[number][] }[] = [];
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
let openHunk: { rows: (typeof rows)[number][] } | null = null;
|
|
105
|
+
|
|
106
|
+
rows.forEach((row, rowIndex) => {
|
|
107
|
+
if (!keep[rowIndex]) {
|
|
108
|
+
openHunk = null;
|
|
109
|
+
|
|
84
110
|
return;
|
|
85
111
|
}
|
|
86
|
-
if (!
|
|
87
|
-
|
|
88
|
-
hunks.push(
|
|
112
|
+
if (!openHunk) {
|
|
113
|
+
openHunk = { rows: [] };
|
|
114
|
+
hunks.push(openHunk);
|
|
89
115
|
}
|
|
90
|
-
|
|
116
|
+
openHunk.rows.push(row);
|
|
91
117
|
});
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
const lines: UnifiedLine[] = [];
|
|
119
|
+
|
|
120
|
+
for (const hunk of hunks) {
|
|
121
|
+
const oldStart = hunk.rows.find((row) => row.oldLine !== null)?.oldLine ?? 1;
|
|
122
|
+
const newStart = hunk.rows.find((row) => row.newLine !== null)?.newLine ?? 1;
|
|
123
|
+
const oldCount = hunk.rows.filter((row) => row.kind !== "add").length;
|
|
124
|
+
const newCount = hunk.rows.filter((row) => row.kind !== "del").length;
|
|
125
|
+
|
|
126
|
+
lines.push({ kind: "hunk", text: `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@` });
|
|
127
|
+
for (const row of hunk.rows) {
|
|
128
|
+
lines.push({
|
|
129
|
+
kind: row.kind,
|
|
130
|
+
text: (row.kind === "add" ? "+" : row.kind === "del" ? "-" : " ") + row.text,
|
|
131
|
+
});
|
|
101
132
|
}
|
|
102
133
|
}
|
|
103
|
-
|
|
134
|
+
|
|
135
|
+
return lines;
|
|
104
136
|
}
|
|
105
137
|
|
|
106
138
|
/** Render a unified diff as text with file headers. */
|
|
107
|
-
export function unifiedDiffText(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
export function unifiedDiffText(
|
|
140
|
+
oldText: string,
|
|
141
|
+
newText: string,
|
|
142
|
+
path = "plan.md",
|
|
143
|
+
context = 3,
|
|
144
|
+
): string | null {
|
|
145
|
+
const lines = unifiedDiff(oldText, newText, context);
|
|
146
|
+
|
|
147
|
+
if (!lines) return null;
|
|
148
|
+
|
|
149
|
+
return [`--- a/${path}`, `+++ b/${path}`, ...lines.map((line) => line.text)].join("\n");
|
|
111
150
|
}
|
|
112
151
|
|
|
113
152
|
export interface EditStats {
|
|
@@ -116,16 +155,12 @@ export interface EditStats {
|
|
|
116
155
|
}
|
|
117
156
|
|
|
118
157
|
export function editStats(oldText: string, newText: string): EditStats {
|
|
119
|
-
const
|
|
120
|
-
|
|
158
|
+
const lines = unifiedDiff(oldText, newText, 0);
|
|
159
|
+
|
|
160
|
+
if (!lines) return { added: 0, removed: 0 };
|
|
161
|
+
|
|
121
162
|
return {
|
|
122
|
-
added:
|
|
123
|
-
removed:
|
|
163
|
+
added: lines.filter((line) => line.kind === "add").length,
|
|
164
|
+
removed: lines.filter((line) => line.kind === "del").length,
|
|
124
165
|
};
|
|
125
166
|
}
|
|
126
|
-
|
|
127
|
-
/** Word-level diff ops for tracked-changes rendering. Whitespace-preserving. */
|
|
128
|
-
export function wordDiff(oldText: string, newText: string): DiffOp[] {
|
|
129
|
-
const tok = (s: string) => s.split(/(\s+)/).filter((x) => x !== "");
|
|
130
|
-
return lcsDiff(tok(oldText), tok(newText));
|
|
131
|
-
}
|
package/src/feedback.test.ts
CHANGED
|
@@ -13,7 +13,9 @@ Review sessions live only in daemon memory today.
|
|
|
13
13
|
Sessions are written as one JSON document per session.
|
|
14
14
|
`;
|
|
15
15
|
|
|
16
|
-
function
|
|
16
|
+
function makeAnnotation(
|
|
17
|
+
partial: Partial<Annotation> & { anchor: Annotation["anchor"] },
|
|
18
|
+
): Annotation {
|
|
17
19
|
return {
|
|
18
20
|
id: "a1",
|
|
19
21
|
kind: "comment",
|
|
@@ -25,91 +27,219 @@ function ann(partial: Partial<Annotation> & { anchor: Annotation["anchor"] }): A
|
|
|
25
27
|
|
|
26
28
|
describe("renderFeedback", () => {
|
|
27
29
|
test("plan edits section carries the one unified diff, directive framing first", () => {
|
|
28
|
-
|
|
30
|
+
// Act
|
|
31
|
+
const feedback = renderFeedback({
|
|
29
32
|
verdictKind: "request_changes",
|
|
30
33
|
summary: "Tighten the storage section.",
|
|
31
34
|
artifactContent: PLAN,
|
|
32
35
|
workingCopy: PLAN.replace("one JSON document", "one JSON record"),
|
|
33
36
|
annotations: [],
|
|
34
|
-
|
|
37
|
+
artifactPath: "docs/plan.md",
|
|
35
38
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
expect(
|
|
39
|
-
expect(
|
|
40
|
-
expect(
|
|
41
|
-
expect(
|
|
42
|
-
expect(
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(feedback).toContain("# Review: request changes");
|
|
42
|
+
expect(feedback).toContain("Tighten the storage section.");
|
|
43
|
+
expect(feedback).toContain("## Plan edits");
|
|
44
|
+
expect(feedback).toContain("Apply this exact diff first");
|
|
45
|
+
expect(feedback).toContain("--- a/docs/plan.md");
|
|
46
|
+
expect(feedback).toContain("-Sessions are written as one JSON document per session.");
|
|
47
|
+
expect(feedback).toContain("+Sessions are written as one JSON record per session.");
|
|
43
48
|
});
|
|
44
49
|
|
|
45
|
-
test("
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
test("reply edits section is labelled for the reply artifact, not a plan", () => {
|
|
51
|
+
// Act
|
|
52
|
+
const feedback = renderFeedback({
|
|
53
|
+
verdictKind: "request_changes",
|
|
48
54
|
summary: "",
|
|
49
55
|
artifactContent: PLAN,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
workingCopy: PLAN.replace("one JSON document", "one JSON record"),
|
|
57
|
+
artifactType: "reply",
|
|
58
|
+
annotations: [],
|
|
59
|
+
artifactPath: "reply.md",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(feedback).toContain("## Reply edits");
|
|
64
|
+
expect(feedback).not.toContain("## Plan edits");
|
|
65
|
+
expect(feedback).toContain("Apply this exact diff first");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a diff working copy is handed back as the curated patch, not a diff of diffs", () => {
|
|
69
|
+
// Arrange
|
|
70
|
+
const submittedPatch = `--- a/src/x.ts
|
|
71
|
+
+++ b/src/x.ts
|
|
72
|
+
@@ -1,1 +1,1 @@
|
|
73
|
+
-const limit = 100;
|
|
74
|
+
+const limit = 250;
|
|
75
|
+
`;
|
|
76
|
+
const curatedPatch = `--- a/src/x.ts
|
|
77
|
+
+++ b/src/x.ts
|
|
78
|
+
@@ -1,1 +1,1 @@
|
|
79
|
+
-const limit = 100;
|
|
80
|
+
+const limit = 175;
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
// Act
|
|
84
|
+
const feedback = renderFeedback({
|
|
85
|
+
verdictKind: "request_changes",
|
|
86
|
+
summary: "",
|
|
87
|
+
artifactContent: submittedPatch,
|
|
88
|
+
workingCopy: curatedPatch,
|
|
89
|
+
artifactType: "diff",
|
|
90
|
+
annotations: [],
|
|
55
91
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
expect(feedback).toContain("## Curated changes");
|
|
95
|
+
expect(feedback).toContain("+const limit = 175;");
|
|
96
|
+
// the submitted patch is not re-diffed against the curated one
|
|
97
|
+
expect(feedback).not.toContain("Plan edits");
|
|
98
|
+
expect(feedback).not.toContain("++const");
|
|
60
99
|
});
|
|
61
100
|
|
|
62
|
-
test("
|
|
63
|
-
|
|
101
|
+
test("a diff working copy that rejected everything says so", () => {
|
|
102
|
+
// Act
|
|
103
|
+
const feedback = renderFeedback({
|
|
64
104
|
verdictKind: "request_changes",
|
|
65
105
|
summary: "",
|
|
106
|
+
artifactContent: "--- a/x\n+++ b/x\n@@ -1 +1 @@\n-a\n+b\n",
|
|
107
|
+
workingCopy: "",
|
|
108
|
+
artifactType: "diff",
|
|
109
|
+
annotations: [],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Assert
|
|
113
|
+
expect(feedback).toContain("## Curated changes");
|
|
114
|
+
expect(feedback).toContain("rejected all of your proposed changes");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("annotations are located by quote and section", () => {
|
|
118
|
+
// Act
|
|
119
|
+
const feedback = renderFeedback({
|
|
120
|
+
verdictKind: "comment",
|
|
121
|
+
summary: "",
|
|
66
122
|
artifactContent: PLAN,
|
|
67
123
|
annotations: [
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
body: "one durable JSON record",
|
|
71
|
-
anchor: { quote: "one JSON document", prefix: "written as ", suffix: " per session." },
|
|
124
|
+
makeAnnotation({
|
|
125
|
+
anchor: { quote: "daemon memory", prefix: "live only in ", suffix: " today." },
|
|
72
126
|
}),
|
|
73
127
|
],
|
|
74
128
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
expect(
|
|
129
|
+
|
|
130
|
+
// Assert
|
|
131
|
+
expect(feedback).toContain("## Annotations (1)");
|
|
132
|
+
expect(feedback).toContain("### 1. Comment (§ Context)");
|
|
133
|
+
expect(feedback).toContain("> daemon memory");
|
|
134
|
+
expect(feedback).toContain("Please clarify.");
|
|
78
135
|
});
|
|
79
136
|
|
|
80
137
|
test("orphaned anchors are flagged, never dropped", () => {
|
|
81
|
-
|
|
138
|
+
// Act
|
|
139
|
+
const feedback = renderFeedback({
|
|
82
140
|
verdictKind: "comment",
|
|
83
141
|
summary: "",
|
|
84
142
|
artifactContent: PLAN,
|
|
85
|
-
annotations: [
|
|
143
|
+
annotations: [
|
|
144
|
+
makeAnnotation({ anchor: { quote: "text that is gone", prefix: "", suffix: "" } }),
|
|
145
|
+
],
|
|
86
146
|
});
|
|
87
|
-
|
|
88
|
-
|
|
147
|
+
|
|
148
|
+
// Assert
|
|
149
|
+
expect(feedback).toContain("[orphaned anchor: the quoted text is no longer present]");
|
|
150
|
+
expect(feedback).toContain("> text that is gone");
|
|
89
151
|
});
|
|
90
152
|
|
|
91
153
|
test("empty review says so explicitly", () => {
|
|
92
|
-
|
|
154
|
+
// Act
|
|
155
|
+
const feedback = renderFeedback({
|
|
93
156
|
verdictKind: "approve",
|
|
94
157
|
summary: "",
|
|
95
158
|
artifactContent: PLAN,
|
|
96
159
|
annotations: [],
|
|
97
160
|
});
|
|
98
|
-
|
|
161
|
+
|
|
162
|
+
// Assert
|
|
163
|
+
expect(feedback).toContain("_No edits or annotations._");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("agent notes never come back as feedback items", () => {
|
|
167
|
+
// Arrange
|
|
168
|
+
const note = makeAnnotation({
|
|
169
|
+
kind: "note",
|
|
170
|
+
anchor: { quote: "src/a.ts", prefix: "", suffix: "" },
|
|
171
|
+
body: "The agent's own explanation of the change.",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Act
|
|
175
|
+
const noteOnly = renderFeedback({
|
|
176
|
+
verdictKind: "approve",
|
|
177
|
+
summary: "",
|
|
178
|
+
artifactContent: PLAN,
|
|
179
|
+
annotations: [note],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Assert
|
|
183
|
+
expect(noteOnly).toContain("_No edits or annotations._");
|
|
184
|
+
expect(noteOnly).not.toContain("The agent's own explanation of the change.");
|
|
185
|
+
const mixed = renderFeedback({
|
|
186
|
+
verdictKind: "request_changes",
|
|
187
|
+
summary: "",
|
|
188
|
+
artifactContent: PLAN,
|
|
189
|
+
annotations: [
|
|
190
|
+
note,
|
|
191
|
+
makeAnnotation({
|
|
192
|
+
anchor: { quote: "one JSON document", prefix: "written as ", suffix: " per session." },
|
|
193
|
+
}),
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(mixed).toContain("## Annotations (1)");
|
|
198
|
+
expect(mixed).not.toContain("The agent's own explanation of the change.");
|
|
99
199
|
});
|
|
100
200
|
|
|
101
201
|
test("anchors resolve against the working copy when present", () => {
|
|
202
|
+
// Arrange
|
|
102
203
|
// annotation made on text that only exists in the edited copy
|
|
103
204
|
const working = PLAN.replace("## Storage", "## Persistence");
|
|
104
|
-
|
|
205
|
+
|
|
206
|
+
// Act
|
|
207
|
+
const feedback = renderFeedback({
|
|
105
208
|
verdictKind: "request_changes",
|
|
106
209
|
summary: "",
|
|
107
210
|
artifactContent: PLAN,
|
|
108
211
|
workingCopy: working,
|
|
109
212
|
annotations: [
|
|
110
|
-
|
|
213
|
+
makeAnnotation({
|
|
214
|
+
anchor: { quote: "one JSON document", prefix: "written as ", suffix: " per session." },
|
|
215
|
+
}),
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Assert
|
|
220
|
+
expect(feedback).toContain("(§ Persistence)");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("a prototype annotation locates by its selector and is never orphan-flagged", () => {
|
|
224
|
+
// Act
|
|
225
|
+
const feedback = renderFeedback({
|
|
226
|
+
verdictKind: "request_changes",
|
|
227
|
+
summary: "",
|
|
228
|
+
artifactType: "prototype",
|
|
229
|
+
artifactContent: "<main><div class='card'>Pricing</div></main>",
|
|
230
|
+
annotations: [
|
|
231
|
+
makeAnnotation({
|
|
232
|
+
body: "Tighten the padding.",
|
|
233
|
+
anchor: { quote: "Pricing", prefix: "", suffix: "", selector: "main > div.card" },
|
|
234
|
+
}),
|
|
111
235
|
],
|
|
236
|
+
artifactPath: "proto.html",
|
|
112
237
|
});
|
|
113
|
-
|
|
238
|
+
|
|
239
|
+
// Assert
|
|
240
|
+
expect(feedback).toContain("(main > div.card)");
|
|
241
|
+
expect(feedback).toContain("Tighten the padding.");
|
|
242
|
+
expect(feedback).toContain("by its CSS selector");
|
|
243
|
+
expect(feedback).not.toContain("orphaned anchor");
|
|
114
244
|
});
|
|
115
245
|
});
|