@henryqw/pi-pr 2.1.2 → 3.0.0
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/README.md
CHANGED
|
@@ -12,6 +12,22 @@ Requires an authenticated GitHub CLI session (`gh auth login`) and a checkout on
|
|
|
12
12
|
|
|
13
13
|
The comment sweep resolves its bundled helper and references from the installed package skill path. It does not require an external `jq` executable.
|
|
14
14
|
|
|
15
|
+
## Feedback snapshots
|
|
16
|
+
|
|
17
|
+
`pr-feedback.mjs fetch --out FILE` prints a compact feedback index. The index
|
|
18
|
+
includes IDs, kinds, states, authors, locations, and parent IDs as needed. It
|
|
19
|
+
does not print comment or review bodies.
|
|
20
|
+
|
|
21
|
+
The saved snapshot still contains the complete feedback. `fetch --json` also
|
|
22
|
+
keeps the complete JSON output. Read one item with
|
|
23
|
+
`pr-feedback.mjs show --snapshot FILE --id ID`.
|
|
24
|
+
|
|
25
|
+
`show` prints one JSON record with its exact stored body and fields. A thread
|
|
26
|
+
record includes child IDs without child bodies. A nested comment includes only
|
|
27
|
+
its parent thread's ID, state, and location. Missing, unknown, duplicate, or
|
|
28
|
+
ambiguous IDs fail. `show` does not run Git, call GitHub, use the network, or
|
|
29
|
+
write files.
|
|
30
|
+
|
|
15
31
|
## Works with
|
|
16
32
|
|
|
17
33
|
**Requires.** [`@henryqw/pi-herdr`](https://pi.henry.wang/extensions/pi-herdr) is the shared Herdr CLI client. It installs with this package.
|
package/extensions/pr-ui.ts
CHANGED
|
@@ -95,6 +95,7 @@ export function formatPrFooter(display: PrDisplay, theme: PrTheme): string | und
|
|
|
95
95
|
|
|
96
96
|
export function formatPrWidget(display: PrDisplay, theme?: PrTheme, width?: number): string[] | undefined {
|
|
97
97
|
if (display.widget === undefined) return undefined;
|
|
98
|
+
if (width !== undefined && width <= 0) return [];
|
|
98
99
|
const color = display.footer?.color ?? "accent";
|
|
99
100
|
const icon = color === "error" ? "✗" : color === "warning" ? "!" : color === "success" ? "✓" : "●";
|
|
100
101
|
const line = `${theme ? theme.fg(color, icon) : icon} ${display.widget}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-pr",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Run /pr to automatically choose the current pull request's next safe lifecycle step: create, update, address feedback, fix CI, or merge when ready.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -28,15 +28,23 @@ Run standalone; never invoke, defer to, or modify Shipyard.
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Target verification requires authenticated `gh`, an open PR, exact local/PR
|
|
31
|
-
head, and one matching configured push target. Inspect base
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
2. Initial fetch prints
|
|
35
|
-
|
|
36
|
-
snapshot
|
|
31
|
+
head, and one matching configured push target. Inspect the base and full
|
|
32
|
+
diff. Maintain the recovery reference's one-line checkpoint after each phase.
|
|
33
|
+
|
|
34
|
+
2. Initial fetch prints a compact index of every feedback ID. Later fetches into
|
|
35
|
+
a copied snapshot print compact indexes of additions, edits, and state changes.
|
|
36
|
+
The snapshot remains complete. For each non-obvious item, inspect only that item:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node "<skill>/scripts/pr-feedback.mjs" show --snapshot "$SNAPSHOT" --id "$ID"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Always inspect every unresolved feedback item before triage. Never
|
|
43
|
+
read or print the whole raw snapshot when `show` provides the bounded lookup.
|
|
37
44
|
Follow [Thread triage](<skill>/references/thread-triage.md>) and ledger every item as
|
|
38
|
-
`actionable`, `non-actionable`, or `blocked`, with evidence, smallest
|
|
39
|
-
and regression. Never reconstruct GraphQL pagination.
|
|
45
|
+
`actionable`, `non-actionable`, or `blocked`, with terse evidence, smallest
|
|
46
|
+
fix, and regression. Never reconstruct GraphQL pagination.
|
|
47
|
+
|
|
40
48
|
3. Verify feedback against code, callers, tests, and PR intent. For SDK/framework
|
|
41
49
|
claims, inspect installed types and runtime caller; missing public capability
|
|
42
50
|
is `blocked`, never a private-API or cast workaround. Invocation authorizes
|
|
@@ -57,6 +65,7 @@ Run standalone; never invoke, defer to, or modify Shipyard.
|
|
|
57
65
|
```bash
|
|
58
66
|
EXPECTED_HEAD=$(git rev-parse --verify 'HEAD^{commit}')
|
|
59
67
|
```
|
|
68
|
+
|
|
60
69
|
6. Copy baseline before final fetch:
|
|
61
70
|
|
|
62
71
|
```bash
|
|
@@ -65,15 +74,18 @@ Run standalone; never invoke, defer to, or modify Shipyard.
|
|
|
65
74
|
node "<skill>/scripts/pr-feedback.mjs" fetch --pr "$PR" --out "$FINAL_SNAPSHOT"
|
|
66
75
|
```
|
|
67
76
|
|
|
68
|
-
Assess
|
|
77
|
+
Assess the compact delta. Use `show --snapshot "$FINAL_SNAPSHOT" --id "$ID"`
|
|
78
|
+
for each non-obvious delta item and every unresolved human feedback item.
|
|
79
|
+
Then resolve all addressed IDs in one command, repeating the flag:
|
|
69
80
|
`node "<skill>/scripts/pr-feedback.mjs" resolve --pr "$PR" --expected-head
|
|
70
|
-
|
|
81
|
+
"$EXPECTED_HEAD" --thread "$ID1" --thread "$ID2"`. Before each resolution,
|
|
71
82
|
the helper re-resolves the checkout's configured push target and exact open PR.
|
|
72
83
|
Never resolve non-actionable or blocked threads. Re-fetch once into `FINAL_SNAPSHOT`, assess
|
|
73
84
|
late delta, batch any newly addressed IDs, then run
|
|
74
85
|
`node "<skill>/scripts/pr-feedback.mjs" checks --pr "$PR" --expected-head
|
|
75
|
-
|
|
86
|
+
"$EXPECTED_HEAD"` once. Read-only
|
|
76
87
|
transient retries are allowed; mutation retries and polling are not. Do not
|
|
77
88
|
reply unless explicitly requested.
|
|
89
|
+
|
|
78
90
|
7. Report `PR | actioned | resolved IDs | skipped IDs | pending IDs | checks |
|
|
79
|
-
|
|
91
|
+
commits | push | blockers`.
|
|
@@ -12,8 +12,9 @@ line under `## PR comment sweep`; never stage it:
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Use phases `triage`, `editing`, `validated`, `committed`, `pushed`, or `resolved`.
|
|
15
|
-
Update after each phase
|
|
16
|
-
|
|
15
|
+
Update after each phase. Keep ledger evidence and validation results terse. Edit
|
|
16
|
+
only fields changed by that phase; do not replace the whole JSON line. Keep
|
|
17
|
+
repository-relative owned paths, commit SHA, push state, and resolved IDs.
|
|
17
18
|
|
|
18
19
|
Resume only when workflow and PR match, snapshot exists and names that PR, local
|
|
19
20
|
`HEAD` equals `head`, and tracked dirty paths equal `owned`. Skip completed
|
|
@@ -630,31 +630,35 @@ function printPrStatus(metadata, log = console.log) {
|
|
|
630
630
|
}
|
|
631
631
|
|
|
632
632
|
function printComments(label, comments, log) {
|
|
633
|
-
log(`${label}=
|
|
633
|
+
log(`${label}=kind\tid\tauthor`);
|
|
634
634
|
for (const comment of comments) {
|
|
635
635
|
if (!isRecord(comment)) throw new FeedbackError("invalid conversation comment");
|
|
636
|
-
|
|
636
|
+
text(comment.body, "conversation comment body");
|
|
637
|
+
log(["conversation_comment", requiredText(comment.id, "conversation comment ID"), login(comment.author)].join("\t"));
|
|
637
638
|
}
|
|
638
639
|
}
|
|
639
640
|
|
|
640
641
|
function printReviews(label, reviews, log) {
|
|
641
|
-
log(`${label}=
|
|
642
|
+
log(`${label}=kind\tid\tstate\tauthor`);
|
|
642
643
|
for (const review of reviews) {
|
|
643
644
|
if (!isRecord(review)) throw new FeedbackError("invalid review");
|
|
644
|
-
|
|
645
|
+
text(review.body, "review body");
|
|
646
|
+
log(["review", requiredText(review.id, "review ID"), requiredText(review.state, "review state"), login(review.author)].join("\t"));
|
|
645
647
|
}
|
|
646
648
|
}
|
|
647
649
|
|
|
648
|
-
function printThreads(label, threads, log) {
|
|
649
|
-
log(`${label}=
|
|
650
|
+
function printThreads(label, threads, log, commentsOnly = false) {
|
|
651
|
+
log(`${label}=kind\tid\tstate\tauthor\tlocation\tparent`);
|
|
650
652
|
for (const thread of threads) {
|
|
653
|
+
if (!isRecord(thread)) throw new FeedbackError("invalid review thread");
|
|
651
654
|
const threadId = requiredText(thread.id, "review thread ID");
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
if (!
|
|
655
|
-
for (const comment of comments) {
|
|
655
|
+
const state = threadState(thread);
|
|
656
|
+
const threadLocation = location(thread);
|
|
657
|
+
if (!commentsOnly) log(["thread", threadId, state, "-", threadLocation, "-"].join("\t"));
|
|
658
|
+
for (const comment of list(thread.comments, `replies for ${threadId}`)) {
|
|
656
659
|
if (!isRecord(comment)) throw new FeedbackError(`invalid comment in ${threadId}`);
|
|
657
|
-
|
|
660
|
+
text(comment.body, "comment body");
|
|
661
|
+
log(["thread_comment", requiredText(comment.id, "comment ID"), state, login(comment.author), threadLocation, threadId].join("\t"));
|
|
658
662
|
}
|
|
659
663
|
}
|
|
660
664
|
}
|
|
@@ -664,6 +668,74 @@ function threadState(thread) {
|
|
|
664
668
|
return bool(thread.isOutdated, "review thread isOutdated") ? "outdated" : "current";
|
|
665
669
|
}
|
|
666
670
|
|
|
671
|
+
function feedbackEntries(data) {
|
|
672
|
+
const entries = [];
|
|
673
|
+
for (const node of list(data.conversationComments, "conversation comments")) {
|
|
674
|
+
if (!isRecord(node)) throw new FeedbackError("invalid conversation comment");
|
|
675
|
+
text(node.body, "conversation comment body");
|
|
676
|
+
login(node.author);
|
|
677
|
+
entries.push({ id: requiredText(node.id, "conversation comment ID"), kind: "conversation_comment", node });
|
|
678
|
+
}
|
|
679
|
+
for (const node of list(data.reviews, "reviews")) {
|
|
680
|
+
if (!isRecord(node)) throw new FeedbackError("invalid review");
|
|
681
|
+
requiredText(node.state, "review state");
|
|
682
|
+
text(node.body, "review body");
|
|
683
|
+
login(node.author);
|
|
684
|
+
entries.push({ id: requiredText(node.id, "review ID"), kind: "review", node });
|
|
685
|
+
}
|
|
686
|
+
for (const thread of list(data.reviewThreads, "review threads")) {
|
|
687
|
+
if (!isRecord(thread)) throw new FeedbackError("invalid review thread");
|
|
688
|
+
const threadId = requiredText(thread.id, "review thread ID");
|
|
689
|
+
threadState(thread);
|
|
690
|
+
location(thread);
|
|
691
|
+
entries.push({ id: threadId, kind: "thread", node: thread });
|
|
692
|
+
for (const node of list(thread.comments, `replies for ${threadId}`)) {
|
|
693
|
+
if (!isRecord(node)) throw new FeedbackError(`invalid comment in ${threadId}`);
|
|
694
|
+
text(node.body, "comment body");
|
|
695
|
+
login(node.author);
|
|
696
|
+
entries.push({ id: requiredText(node.id, "comment ID"), kind: "thread_comment", node, thread });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const byId = new Map();
|
|
701
|
+
for (const entry of entries) {
|
|
702
|
+
const matches = byId.get(entry.id) ?? [];
|
|
703
|
+
matches.push(entry);
|
|
704
|
+
byId.set(entry.id, matches);
|
|
705
|
+
}
|
|
706
|
+
for (const [id, matches] of byId) {
|
|
707
|
+
if (matches.length < 2) continue;
|
|
708
|
+
if (new Set(matches.map(({ kind }) => kind)).size > 1) throw new FeedbackError(`ambiguous feedback ID: ${id}`);
|
|
709
|
+
throw new FeedbackError(`duplicate feedback ID: ${id}`);
|
|
710
|
+
}
|
|
711
|
+
return byId;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function showFeedback(options, log = console.log) {
|
|
715
|
+
const data = snapshot(resolve(options.snapshot));
|
|
716
|
+
const matches = feedbackEntries(data).get(options.id);
|
|
717
|
+
if (!matches) throw new FeedbackError(`feedback ID not found: ${options.id}`);
|
|
718
|
+
const [{ kind, node, thread }] = matches;
|
|
719
|
+
let record;
|
|
720
|
+
if (kind === "thread") {
|
|
721
|
+
const { comments, ...metadata } = node;
|
|
722
|
+
record = { ...metadata, kind, childIds: nodeIds(comments, `replies for ${node.id}`) };
|
|
723
|
+
} else if (kind === "thread_comment") {
|
|
724
|
+
record = {
|
|
725
|
+
...node,
|
|
726
|
+
kind,
|
|
727
|
+
parentThread: {
|
|
728
|
+
id: requiredText(thread.id, "review thread ID"),
|
|
729
|
+
state: threadState(thread),
|
|
730
|
+
location: location(thread),
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
} else {
|
|
734
|
+
record = { ...node, kind };
|
|
735
|
+
}
|
|
736
|
+
log(JSON.stringify(record));
|
|
737
|
+
}
|
|
738
|
+
|
|
667
739
|
function printFeedbackDelta(data, previous, log) {
|
|
668
740
|
const oldComments = new Map(previous.conversationComments.map((comment) => [requiredText(comment.id, "previous conversation comment ID"), comment]));
|
|
669
741
|
const oldReviews = new Map(previous.reviews.map((review) => [requiredText(review.id, "previous review ID"), review]));
|
|
@@ -689,7 +761,7 @@ function printFeedbackDelta(data, previous, log) {
|
|
|
689
761
|
if (updated.length) updatedReplies.push({ ...thread, comments: updated });
|
|
690
762
|
const before = threadState(oldThread);
|
|
691
763
|
const after = threadState(thread);
|
|
692
|
-
if (before !== after) stateChanges.push([thread
|
|
764
|
+
if (before !== after) stateChanges.push([thread, before, after]);
|
|
693
765
|
}
|
|
694
766
|
|
|
695
767
|
if (newComments.length) printComments("new_conversation_comments", newComments, log);
|
|
@@ -697,17 +769,19 @@ function printFeedbackDelta(data, previous, log) {
|
|
|
697
769
|
if (newReviews.length) printReviews("new_reviews", newReviews, log);
|
|
698
770
|
if (updatedReviews.length) printReviews("updated_reviews", updatedReviews, log);
|
|
699
771
|
if (newThreads.length) printThreads("new_threads", newThreads, log);
|
|
700
|
-
if (newReplies.length) printThreads("new_replies", newReplies, log);
|
|
701
|
-
if (updatedReplies.length) printThreads("updated_replies", updatedReplies, log);
|
|
772
|
+
if (newReplies.length) printThreads("new_replies", newReplies, log, true);
|
|
773
|
+
if (updatedReplies.length) printThreads("updated_replies", updatedReplies, log, true);
|
|
702
774
|
if (stateChanges.length) {
|
|
703
|
-
log("thread_state_changes=
|
|
704
|
-
for (const
|
|
775
|
+
log("thread_state_changes=kind\tid\tbefore\tafter\tlocation");
|
|
776
|
+
for (const [thread, before, after] of stateChanges) {
|
|
777
|
+
log(["thread", requiredText(thread.id, "review thread ID"), before, after, location(thread)].join("\t"));
|
|
778
|
+
}
|
|
705
779
|
}
|
|
706
780
|
if (![newComments, updatedComments, newReviews, updatedReviews, newThreads, newReplies, updatedReplies, stateChanges].some((items) => items.length)) log("feedback_delta=none");
|
|
707
781
|
}
|
|
708
782
|
|
|
709
783
|
function printFetchSummary(data, previous, out, log = console.log) {
|
|
710
|
-
const {
|
|
784
|
+
const { outdated, openCurrent } = threadBuckets(data.reviewThreads);
|
|
711
785
|
log(`snapshot=${out}`);
|
|
712
786
|
log(`counts comments=${data.conversationComments.length} reviews=${data.reviews.length} threads=${data.reviewThreads.length} open_current=${openCurrent.length} outdated=${outdated.length}`);
|
|
713
787
|
printPrStatus(data.pullRequest, log);
|
|
@@ -717,9 +791,7 @@ function printFetchSummary(data, previous, out, log = console.log) {
|
|
|
717
791
|
}
|
|
718
792
|
printComments("conversation_comments", data.conversationComments, log);
|
|
719
793
|
printReviews("reviews", data.reviews, log);
|
|
720
|
-
printThreads("
|
|
721
|
-
printThreads("outdated_threads", outdated, log);
|
|
722
|
-
log(`resolved_thread_ids=${nodeIds(resolved, "resolved review thread").join(",") || "-"}`);
|
|
794
|
+
printThreads("review_threads", data.reviewThreads, log);
|
|
723
795
|
}
|
|
724
796
|
|
|
725
797
|
function writeSnapshot(path, data) {
|
|
@@ -1365,6 +1437,7 @@ exit 1
|
|
|
1365
1437
|
{ id: "resolved", isResolved: false, isOutdated: false, path: "src/c.js", line: 5, comments: [] },
|
|
1366
1438
|
],
|
|
1367
1439
|
};
|
|
1440
|
+
const botBody = "<details>verbose bot HTML</details>";
|
|
1368
1441
|
const feedback = {
|
|
1369
1442
|
pullRequest: {
|
|
1370
1443
|
mergeStateStatus: "BLOCKED",
|
|
@@ -1376,7 +1449,7 @@ exit 1
|
|
|
1376
1449
|
],
|
|
1377
1450
|
},
|
|
1378
1451
|
conversationComments: [{ ...previousFeedback.conversationComments[0], body: "edited conversation" }, { id: "conversation-2", author: { login: "octocat" }, body: "new conversation" }],
|
|
1379
|
-
reviews: [{ ...previousFeedback.reviews[0], state: "DISMISSED", body: "edited review" }, { id: "review-2", state: "COMMENTED", author: { login: "
|
|
1452
|
+
reviews: [{ ...previousFeedback.reviews[0], state: "DISMISSED", body: "edited review" }, { id: "review-2", state: "COMMENTED", author: { login: "review-bot[bot]" }, body: botBody }],
|
|
1380
1453
|
reviewThreads: [
|
|
1381
1454
|
{ ...previousFeedback.reviewThreads[0], comments: [{ ...previousFeedback.reviewThreads[0].comments[0], body: "edited reply" }, { id: "new-reply", author: { login: "reviewer" }, body: "new reply" }] },
|
|
1382
1455
|
{ ...previousFeedback.reviewThreads[1], isOutdated: true },
|
|
@@ -1395,8 +1468,8 @@ exit 1
|
|
|
1395
1468
|
comments: [],
|
|
1396
1469
|
}], (line) => maliciousPathLines.push(line));
|
|
1397
1470
|
assert.deepEqual(maliciousPathLines, [
|
|
1398
|
-
"threads=
|
|
1399
|
-
"thread\tcurrent\tsrc/forged\\nrow\\t\\u001b[31m\\u009b31m.js:7\t
|
|
1471
|
+
"threads=kind\tid\tstate\tauthor\tlocation\tparent",
|
|
1472
|
+
"thread\tthread\tcurrent\t-\tsrc/forged\\nrow\\t\\u001b[31m\\u009b31m.js:7\t-",
|
|
1400
1473
|
]);
|
|
1401
1474
|
|
|
1402
1475
|
assert.equal(checkBucket({ conclusion: "STALE" }), "failing");
|
|
@@ -1405,27 +1478,95 @@ exit 1
|
|
|
1405
1478
|
assert.throws(() => checkBucket({ status: "IN_PROGRESS", conclusion: "SUCCESS" }), /invalid PR status check/);
|
|
1406
1479
|
assert.throws(() => checkBucket({ status: "", state: "", conclusion: "" }), /invalid PR status check/);
|
|
1407
1480
|
assert.deepEqual(checkSummary(feedback.pullRequest), { passing: 1, pending: 1, failing: 2 });
|
|
1481
|
+
const bodies = ["edited conversation", "edited review", "edited reply", "new conversation", botBody, "new reply", "new thread"];
|
|
1408
1482
|
const initialLines = [];
|
|
1409
1483
|
printFetchSummary(feedback, null, "/tmp/snapshot", (line) => initialLines.push(line));
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1484
|
+
assert.deepEqual(initialLines.slice(0, 3), [
|
|
1485
|
+
"snapshot=/tmp/snapshot",
|
|
1486
|
+
"counts comments=2 reviews=2 threads=4 open_current=2 outdated=1",
|
|
1487
|
+
"merge_state=BLOCKED checks_total=4 passing=1 pending=1 failing=2",
|
|
1488
|
+
]);
|
|
1489
|
+
const initial = initialLines.join("\n");
|
|
1490
|
+
for (const body of bodies) assert(!initial.includes(body));
|
|
1491
|
+
for (const id of [
|
|
1492
|
+
"conversation-1", "conversation-2", "review-1", "review-2", "current", "current-comment", "new-reply",
|
|
1493
|
+
"outdated", "resolved", "new-thread", "new-thread-comment",
|
|
1494
|
+
]) assert(initial.includes(id), id);
|
|
1495
|
+
assert(initial.includes("thread_comment\tnew-reply\tcurrent\treviewer\tsrc/a.js:3\tcurrent"));
|
|
1496
|
+
assert(initial.includes("thread\tresolved\tresolved\t-\tsrc/c.js:5\t-"));
|
|
1497
|
+
|
|
1498
|
+
const savedFeedback = {
|
|
1499
|
+
...feedback,
|
|
1500
|
+
pullRequest: { ...feedback.pullRequest, url: "https://github.com/owner/repo/pull/1" },
|
|
1501
|
+
openCurrentThreads: threadBuckets(feedback.reviewThreads).openCurrent,
|
|
1502
|
+
};
|
|
1415
1503
|
const savedPrevious = { ...previousFeedback, pullRequest: savedFeedback.pullRequest };
|
|
1416
1504
|
const deltaSnapshot = join(temporary, "delta.json");
|
|
1417
1505
|
writeSnapshot(deltaSnapshot, savedPrevious);
|
|
1418
1506
|
const deltaLines = [];
|
|
1419
1507
|
printFetchSummary(feedback, previousSnapshot(deltaSnapshot), deltaSnapshot, (line) => deltaLines.push(line));
|
|
1420
1508
|
writeSnapshot(deltaSnapshot, savedFeedback);
|
|
1421
|
-
assert.
|
|
1509
|
+
assert.deepEqual(snapshot(deltaSnapshot), savedFeedback);
|
|
1422
1510
|
const delta = deltaLines.join("\n");
|
|
1511
|
+
assert(delta.includes(`snapshot=${deltaSnapshot}`));
|
|
1512
|
+
assert(delta.includes("counts comments=2 reviews=2 threads=4 open_current=2 outdated=1"));
|
|
1423
1513
|
assert(delta.includes("merge_state=BLOCKED checks_total=4 passing=1 pending=1 failing=2"));
|
|
1424
|
-
for (const body of [
|
|
1425
|
-
for (const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
assert(delta.includes("
|
|
1514
|
+
for (const body of [...bodies, "old conversation", "old review", "old reply"]) assert(!delta.includes(body));
|
|
1515
|
+
for (const id of ["conversation-1", "conversation-2", "review-1", "review-2", "current-comment", "new-reply", "new-thread", "new-thread-comment"]) {
|
|
1516
|
+
assert(delta.includes(id), id);
|
|
1517
|
+
}
|
|
1518
|
+
assert(delta.includes("new_replies=kind\tid\tstate\tauthor\tlocation\tparent"));
|
|
1519
|
+
assert(delta.includes("thread_comment\tnew-reply\tcurrent\treviewer\tsrc/a.js:3\tcurrent"));
|
|
1520
|
+
assert(!delta.includes("new_replies=kind\tid\tstate\tauthor\tlocation\tparent\nthread\tcurrent"));
|
|
1521
|
+
assert(delta.includes("thread\toutdated\tcurrent\toutdated\tsrc/b.js:4"));
|
|
1522
|
+
assert(delta.includes("thread\tresolved\tcurrent\tresolved\tsrc/c.js:5"));
|
|
1523
|
+
|
|
1524
|
+
const show = (id) => {
|
|
1525
|
+
const lines = [];
|
|
1526
|
+
showFeedback({ snapshot: deltaSnapshot, id }, (line) => lines.push(line));
|
|
1527
|
+
assert.equal(lines.length, 1);
|
|
1528
|
+
return { raw: lines[0], record: JSON.parse(lines[0]) };
|
|
1529
|
+
};
|
|
1530
|
+
const shownComment = show("conversation-2");
|
|
1531
|
+
assert.deepEqual(shownComment.record, { ...feedback.conversationComments[1], kind: "conversation_comment" });
|
|
1532
|
+
const shownReview = show("review-2");
|
|
1533
|
+
assert.deepEqual(shownReview.record, { ...feedback.reviews[1], kind: "review" });
|
|
1534
|
+
const shownReply = show("current-comment");
|
|
1535
|
+
assert.deepEqual(shownReply.record, {
|
|
1536
|
+
...feedback.reviewThreads[0].comments[0],
|
|
1537
|
+
kind: "thread_comment",
|
|
1538
|
+
parentThread: { id: "current", state: "current", location: "src/a.js:3" },
|
|
1539
|
+
});
|
|
1540
|
+
const { comments: _comments, ...currentThread } = feedback.reviewThreads[0];
|
|
1541
|
+
const shownThread = show("current");
|
|
1542
|
+
assert.deepEqual(shownThread.record, { ...currentThread, kind: "thread", childIds: ["current-comment", "new-reply"] });
|
|
1543
|
+
for (const [shown, selectedBody] of [
|
|
1544
|
+
[shownComment.raw, "new conversation"],
|
|
1545
|
+
[shownReview.raw, botBody],
|
|
1546
|
+
[shownReply.raw, "edited reply"],
|
|
1547
|
+
[shownThread.raw, null],
|
|
1548
|
+
]) {
|
|
1549
|
+
for (const body of bodies) assert.equal(shown.includes(body), body === selectedBody, `${body} in ${shown}`);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
assert.throws(() => showFeedback({ snapshot: deltaSnapshot, id: "unknown" }, () => {}), /feedback ID not found/);
|
|
1553
|
+
const invalidSnapshot = join(temporary, "invalid.json");
|
|
1554
|
+
writeSnapshot(invalidSnapshot, { ...savedFeedback, conversationComments: [{ author: null, body: "invalid" }] });
|
|
1555
|
+
assert.throws(() => showFeedback({ snapshot: invalidSnapshot, id: "anything" }, () => {}), /conversation comment ID must be a string/);
|
|
1556
|
+
const duplicateSnapshot = join(temporary, "duplicate.json");
|
|
1557
|
+
writeSnapshot(duplicateSnapshot, { ...savedFeedback, conversationComments: [feedback.conversationComments[0], feedback.conversationComments[0]] });
|
|
1558
|
+
assert.throws(() => showFeedback({ snapshot: duplicateSnapshot, id: "conversation-1" }, () => {}), /duplicate feedback ID/);
|
|
1559
|
+
const ambiguousSnapshot = join(temporary, "ambiguous.json");
|
|
1560
|
+
writeSnapshot(ambiguousSnapshot, {
|
|
1561
|
+
...savedFeedback,
|
|
1562
|
+
conversationComments: [{ ...feedback.conversationComments[0], id: "review-1" }],
|
|
1563
|
+
});
|
|
1564
|
+
assert.throws(() => showFeedback({ snapshot: ambiguousSnapshot, id: "review-1" }, () => {}), /ambiguous feedback ID/);
|
|
1565
|
+
assert.throws(() => parse("show", ["--snapshot", deltaSnapshot]), /show needs --id/);
|
|
1566
|
+
assert.throws(() => parse("show", ["--snapshot", deltaSnapshot, "--id", ""]), /--id must not be empty/);
|
|
1567
|
+
assert.throws(() => parse("show", ["--snapshot", deltaSnapshot, "--id", "current", "--unknown"]), /unsupported --unknown/);
|
|
1568
|
+
assert.throws(() => parse("show", ["--snapshot", deltaSnapshot, "--id", "current", "--id", "current"]), /--id was supplied twice/);
|
|
1569
|
+
|
|
1429
1570
|
const unchangedLines = [];
|
|
1430
1571
|
printFetchSummary(feedback, savedFeedback, deltaSnapshot, (line) => unchangedLines.push(line));
|
|
1431
1572
|
assert(unchangedLines.includes("feedback_delta=none"));
|
|
@@ -1587,6 +1728,7 @@ function usage() {
|
|
|
1587
1728
|
${command} fetch [--pr PR] (--out FILE | --json)
|
|
1588
1729
|
${command} target [--pr PR]
|
|
1589
1730
|
${command} checks [--pr PR] --expected-head SHA
|
|
1731
|
+
${command} show --snapshot FILE --id ID
|
|
1590
1732
|
${command} snapshot-url --snapshot FILE
|
|
1591
1733
|
${command} push --snapshot FILE
|
|
1592
1734
|
${command} resolve [--pr PR] --expected-head SHA --thread ID [--thread ID ...]
|
|
@@ -1598,6 +1740,7 @@ function parse(command, args) {
|
|
|
1598
1740
|
fetch: new Set(["--pr", "--out", "--json"]),
|
|
1599
1741
|
target: new Set(["--pr"]),
|
|
1600
1742
|
checks: new Set(["--pr", "--expected-head"]),
|
|
1743
|
+
show: new Set(["--snapshot", "--id"]),
|
|
1601
1744
|
"snapshot-url": new Set(["--snapshot"]),
|
|
1602
1745
|
push: new Set(["--snapshot"]),
|
|
1603
1746
|
resolve: new Set(["--pr", "--expected-head", "--thread"]),
|
|
@@ -1626,9 +1769,11 @@ function parse(command, args) {
|
|
|
1626
1769
|
validatePrRef(values.pr);
|
|
1627
1770
|
if (command === "fetch" && values.out === undefined && !values.json) throw new UsageError("fetch needs --out or --json");
|
|
1628
1771
|
if (command === "fetch" && values.out !== undefined && values.json) throw new UsageError("fetch accepts --out or --json, not both");
|
|
1629
|
-
if (
|
|
1772
|
+
if (["push", "show", "snapshot-url"].includes(command) && values.snapshot === undefined) {
|
|
1630
1773
|
throw new UsageError(`${command} needs --snapshot`);
|
|
1631
1774
|
}
|
|
1775
|
+
if (command === "show" && values.id === undefined) throw new UsageError("show needs --id");
|
|
1776
|
+
if (command === "show" && !values.id) throw new UsageError("--id must not be empty");
|
|
1632
1777
|
if (command === "checks" && values.expectedHead === undefined) throw new UsageError("checks needs --expected-head");
|
|
1633
1778
|
if ((command === "checks" || command === "resolve") && values.expectedHead !== undefined) {
|
|
1634
1779
|
if (!OID.test(values.expectedHead)) throw new UsageError("--expected-head must be a full OID");
|
|
@@ -1657,6 +1802,7 @@ function main(argv) {
|
|
|
1657
1802
|
if (command === "fetch") fetchFeedback(options);
|
|
1658
1803
|
if (command === "target") verifyTarget(options);
|
|
1659
1804
|
if (command === "checks") checkPr(options);
|
|
1805
|
+
if (command === "show") showFeedback(options);
|
|
1660
1806
|
if (command === "snapshot-url") printSnapshotPrUrl(options);
|
|
1661
1807
|
if (command === "push") pushHead(options);
|
|
1662
1808
|
if (command === "resolve") resolveFeedback(options);
|