feat(web): show file statuses in review (#1683)
* feat(web): show file statuses in review * fix(web): address review status feedback * fix(web): deduplicate review sources * fix(web): align review progress identities * fix(web): guard review status labels * fix(web): enforce file status exhaustiveness * perf(web): index review progress sources * fix(web): preserve files during status hydration * fix(web): align multi-repo review progress
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildPrByRepoMap, mapPRFilesToChangedFiles } from "./changes-panel-helpers";
|
||||
import {
|
||||
buildPrByRepoMap,
|
||||
computeReviewProgress,
|
||||
mapPRFilesToChangedFiles,
|
||||
selectPRFilesForReviewProgress,
|
||||
type ReviewProgressPRFile,
|
||||
} from "./changes-panel-helpers";
|
||||
import { hashDiff, resolvePRReviewRepositoryName, reviewFileKey } from "@/components/review/types";
|
||||
import type { PRDiffFile } from "@/lib/types/github";
|
||||
|
||||
const PRIMARY_PR_ID = "primary-pr";
|
||||
const RENAMED_PATH = "src/renamed.ts";
|
||||
|
||||
function diffFile(overrides: Partial<PRDiffFile>): PRDiffFile {
|
||||
return {
|
||||
filename: "src/app.ts",
|
||||
@@ -13,6 +23,28 @@ function diffFile(overrides: Partial<PRDiffFile>): PRDiffFile {
|
||||
} as PRDiffFile;
|
||||
}
|
||||
|
||||
function progressPRFile(
|
||||
overrides: Partial<PRDiffFile> & { repository_name?: string },
|
||||
): ReviewProgressPRFile {
|
||||
return {
|
||||
...diffFile(overrides),
|
||||
repository_name: overrides.repository_name,
|
||||
};
|
||||
}
|
||||
|
||||
function progressPRSource(repositoryName: string, filename: string) {
|
||||
return { repositoryName, files: [diffFile({ filename })] };
|
||||
}
|
||||
|
||||
type ProgressLocalFile = Parameters<typeof computeReviewProgress>[0][number];
|
||||
|
||||
function progressLocalFile(
|
||||
path: string,
|
||||
overrides: Partial<ProgressLocalFile> = {},
|
||||
): ProgressLocalFile {
|
||||
return { path, status: "modified", staged: false, diff: "", ...overrides };
|
||||
}
|
||||
|
||||
describe("mapPRFilesToChangedFiles", () => {
|
||||
it("maps GitHub status strings to FileInfo statuses", () => {
|
||||
const out = mapPRFilesToChangedFiles([
|
||||
@@ -20,8 +52,10 @@ describe("mapPRFilesToChangedFiles", () => {
|
||||
diffFile({ filename: "b.ts", status: "removed" }),
|
||||
diffFile({ filename: "c.ts", status: "renamed", old_path: "old/c.ts" }),
|
||||
diffFile({ filename: "d.ts", status: "modified" }),
|
||||
// Anything not in the explicit set should fall through to "modified".
|
||||
diffFile({ filename: "e.ts", status: "weird" as PRDiffFile["status"] }),
|
||||
diffFile({ filename: "e.ts", status: "copied" }),
|
||||
diffFile({ filename: "f.ts", status: "changed" }),
|
||||
diffFile({ filename: "g.ts", status: "unchanged" }),
|
||||
diffFile({ filename: "h.ts", status: "weird" as PRDiffFile["status"] }),
|
||||
]);
|
||||
expect(out.map((f) => f.status)).toEqual([
|
||||
"added",
|
||||
@@ -29,6 +63,9 @@ describe("mapPRFilesToChangedFiles", () => {
|
||||
"renamed",
|
||||
"modified",
|
||||
"modified",
|
||||
"modified",
|
||||
"modified",
|
||||
"modified",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -69,6 +106,285 @@ describe("mapPRFilesToChangedFiles", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectPRFilesForReviewProgress", () => {
|
||||
it("selects only the primary PR and qualifies it in a multi-repo task", () => {
|
||||
const repositoryName = resolvePRReviewRepositoryName(
|
||||
{ repository_id: "repo-frontend", repo: "widgets" },
|
||||
"acme/widgets",
|
||||
);
|
||||
const files = selectPRFilesForReviewProgress(
|
||||
new Map([
|
||||
[PRIMARY_PR_ID, progressPRSource(repositoryName ?? "widgets", "README.md")],
|
||||
["secondary-pr", progressPRSource("backend", "README.md")],
|
||||
]),
|
||||
PRIMARY_PR_ID,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(files.map((file) => reviewFileKey({ path: file.filename, ...file }))).toEqual([
|
||||
"acme-widgets\u0000README.md",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the selected primary PR bare in a legacy single-repo task", () => {
|
||||
const files = selectPRFilesForReviewProgress(
|
||||
new Map([[PRIMARY_PR_ID, progressPRSource("kandev", "README.md")]]),
|
||||
PRIMARY_PR_ID,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(files.map((file) => reviewFileKey({ path: file.filename, ...file }))).toEqual([
|
||||
"README.md",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeReviewProgress PR files", () => {
|
||||
it("counts same-path PR reviews per repository using each patch hash", () => {
|
||||
const path = "README.md";
|
||||
const frontendPatch = "@@ -1 +1 @@\n-frontend old\n+frontend new";
|
||||
const backendPatch = "@@ -1 +1 @@\n-backend old\n+backend new";
|
||||
const frontendKey = reviewFileKey({ path, repository_name: "frontend" });
|
||||
const backendKey = reviewFileKey({ path, repository_name: "backend" });
|
||||
|
||||
const progress = computeReviewProgress(
|
||||
[],
|
||||
null,
|
||||
new Map([
|
||||
[frontendKey, { reviewed: true, diffHash: hashDiff(frontendPatch) }],
|
||||
[backendKey, { reviewed: true, diffHash: hashDiff(backendPatch) }],
|
||||
]),
|
||||
[
|
||||
progressPRFile({ filename: path, patch: frontendPatch, repository_name: "frontend" }),
|
||||
progressPRFile({ filename: path, patch: backendPatch, repository_name: "backend" }),
|
||||
],
|
||||
);
|
||||
|
||||
expect(progress).toEqual({ reviewedCount: 2, totalFileCount: 2 });
|
||||
});
|
||||
|
||||
it("keeps a same-repo uncommitted file over PR data without dropping another repo", () => {
|
||||
const path = "README.md";
|
||||
const frontendDiff = "@@ -1 +1 @@\n-local old\n+local new";
|
||||
const backendPatch = "@@ -1 +1 @@\n-backend old\n+backend new";
|
||||
const frontendKey = reviewFileKey({ path, repository_name: "frontend" });
|
||||
const backendKey = reviewFileKey({ path, repository_name: "backend" });
|
||||
|
||||
const progress = computeReviewProgress(
|
||||
[progressLocalFile(path, { diff: frontendDiff, repository_name: "frontend" })],
|
||||
null,
|
||||
new Map([
|
||||
[frontendKey, { reviewed: true, diffHash: hashDiff(frontendDiff) }],
|
||||
[backendKey, { reviewed: true, diffHash: hashDiff(backendPatch) }],
|
||||
]),
|
||||
[
|
||||
progressPRFile({
|
||||
filename: path,
|
||||
patch: "@@ -1 +1 @@\n-pr old\n+pr new",
|
||||
repository_name: "frontend",
|
||||
}),
|
||||
progressPRFile({ filename: path, patch: backendPatch, repository_name: "backend" }),
|
||||
],
|
||||
);
|
||||
|
||||
expect(progress).toEqual({ reviewedCount: 2, totalFileCount: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeReviewProgress rejected source precedence", () => {
|
||||
const uncommittedWinner: Parameters<typeof computeReviewProgress>[0] = [
|
||||
progressLocalFile(RENAMED_PATH, { status: "renamed", old_path: "src/old.ts" }),
|
||||
];
|
||||
const cumulativeWinner: Parameters<typeof computeReviewProgress>[1] = {
|
||||
files: { [RENAMED_PATH]: { path: RENAMED_PATH, status: "renamed", diff: "" } },
|
||||
};
|
||||
|
||||
it.each([
|
||||
{ name: "uncommitted", uncommittedFiles: uncommittedWinner, cumulativeDiff: null },
|
||||
{ name: "cumulative", uncommittedFiles: [], cumulativeDiff: cumulativeWinner },
|
||||
])("does not hash a rejected PR patch past a patchless $name winner", (winner) => {
|
||||
const progress = computeReviewProgress(
|
||||
winner.uncommittedFiles,
|
||||
winner.cumulativeDiff,
|
||||
new Map([[RENAMED_PATH, { reviewed: true, diffHash: hashDiff("") }]]),
|
||||
[diffFile({ filename: RENAMED_PATH, patch: "@@rejected@@" })],
|
||||
);
|
||||
|
||||
expect(progress).toEqual({ reviewedCount: 1, totalFileCount: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeReviewProgress source precedence", () => {
|
||||
it("keeps bare uncommitted precedence over a stamped cumulative file at the same path", () => {
|
||||
const path = RENAMED_PATH;
|
||||
const progress = computeReviewProgress(
|
||||
[
|
||||
{
|
||||
path,
|
||||
status: "renamed",
|
||||
staged: false,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
old_path: "src/old.ts",
|
||||
diff: "",
|
||||
},
|
||||
],
|
||||
{
|
||||
files: {
|
||||
[`frontend\u0000${path}`]: {
|
||||
path,
|
||||
repository_name: "frontend",
|
||||
diff: "@@ -1 +1 @@\n-old\n+new",
|
||||
},
|
||||
},
|
||||
},
|
||||
new Map([[path, { reviewed: true }]]),
|
||||
);
|
||||
|
||||
expect(progress).toEqual({ reviewedCount: 1, totalFileCount: 1 });
|
||||
});
|
||||
|
||||
it("keeps same-path composite keys from different repositories distinct", () => {
|
||||
const path = "README.md";
|
||||
const progress = computeReviewProgress(
|
||||
[
|
||||
{
|
||||
path,
|
||||
status: "modified",
|
||||
staged: false,
|
||||
diff: "",
|
||||
repository_name: "frontend",
|
||||
},
|
||||
],
|
||||
{
|
||||
files: {
|
||||
[`backend\u0000${path}`]: {
|
||||
path,
|
||||
repository_name: "backend",
|
||||
diff: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
new Map(),
|
||||
);
|
||||
|
||||
expect(progress.totalFileCount).toBe(2);
|
||||
});
|
||||
|
||||
it("counts a reviewed patchless file under its repository-qualified review key", () => {
|
||||
const file = {
|
||||
path: RENAMED_PATH,
|
||||
status: "renamed" as const,
|
||||
staged: false,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
old_path: "src/old.ts",
|
||||
diff: "",
|
||||
repository_name: "frontend",
|
||||
};
|
||||
const key = reviewFileKey(file);
|
||||
|
||||
expect(computeReviewProgress([file], null, new Map([[key, { reviewed: true }]]))).toEqual({
|
||||
reviewedCount: 1,
|
||||
totalFileCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("counts patchless-only status and PR files", () => {
|
||||
const progress = computeReviewProgress(
|
||||
[
|
||||
{
|
||||
path: "src/local-renamed.ts",
|
||||
status: "renamed",
|
||||
staged: false,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
old_path: "src/local-old.ts",
|
||||
diff: "",
|
||||
},
|
||||
],
|
||||
null,
|
||||
new Map(),
|
||||
[
|
||||
diffFile({
|
||||
filename: "src/pr-renamed.ts",
|
||||
status: "renamed",
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
old_path: "src/pr-old.ts",
|
||||
patch: "",
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
expect(progress.totalFileCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeReviewProgress repository key mode", () => {
|
||||
it("uses bare local, cumulative, and PR keys for a single named repository", () => {
|
||||
const localPath = "src/local.ts";
|
||||
const cumulativePath = "src/cumulative.ts";
|
||||
const prPath = "src/pr.ts";
|
||||
const progress = computeReviewProgress(
|
||||
[progressLocalFile(localPath, { repository_name: "frontend", diff: "@@local@@" })],
|
||||
{
|
||||
files: {
|
||||
[`frontend\u0000${cumulativePath}`]: {
|
||||
path: cumulativePath,
|
||||
repository_name: "frontend",
|
||||
diff: "@@cumulative@@",
|
||||
},
|
||||
},
|
||||
},
|
||||
new Map([
|
||||
[localPath, { reviewed: true }],
|
||||
[cumulativePath, { reviewed: true }],
|
||||
[prPath, { reviewed: true }],
|
||||
]),
|
||||
[progressPRFile({ filename: prPath, patch: "@@pr@@" })],
|
||||
false,
|
||||
);
|
||||
|
||||
expect(progress).toEqual({ reviewedCount: 3, totalFileCount: 3 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeReviewProgress scalability", () => {
|
||||
it("indexes cumulative file identities once for large review sets", () => {
|
||||
const fileCount = 80;
|
||||
let pathReads = 0;
|
||||
const files = Object.fromEntries(
|
||||
Array.from({ length: fileCount }, (_, index) => {
|
||||
const path = `src/file-${index}.ts`;
|
||||
return [
|
||||
`frontend\u0000${path}`,
|
||||
{
|
||||
get path() {
|
||||
pathReads++;
|
||||
return path;
|
||||
},
|
||||
repository_name: "frontend",
|
||||
diff: `@@ -1 +1 @@\n-old-${index}\n+new-${index}`,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
const reviews = new Map(
|
||||
Array.from({ length: fileCount }, (_, index) => [
|
||||
`frontend\u0000src/file-${index}.ts`,
|
||||
{ reviewed: true },
|
||||
]),
|
||||
);
|
||||
|
||||
expect(computeReviewProgress([], { files }, reviews)).toEqual({
|
||||
reviewedCount: fileCount,
|
||||
totalFileCount: fileCount,
|
||||
});
|
||||
expect(pathReads).toBeLessThanOrEqual(fileCount * 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPrByRepoMap", () => {
|
||||
it("does not copy a multi-repo TaskPR URL into the empty-key fallback", () => {
|
||||
const map = buildPrByRepoMap(
|
||||
|
||||
Reference in New Issue
Block a user