Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/reload-retired-note-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

`hunk session reload` onto a diff that leaves out a file holding a live note keeps the session connected, and the note returns when a later reload brings its file back.
48 changes: 48 additions & 0 deletions packages/hunk/src/ui/hooks/useTerminalReview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,54 @@ describe("useTerminalReview", () => {
}
});

test("a reload that retires a noted file publishes a count that matches its summaries", async () => {
// The daemon refuses a snapshot whose live comment count differs from its list, so a note
// kept for a retired file must leave both until a later reload brings the file back.
const createAlpha = () =>
createDiffFile("alpha", "alpha.ts", "export const alpha = 1;\n", "export const alpha = 2;\n");
const createBeta = () =>
createDiffFile("beta", "beta.ts", "export const beta = 1;\n", "export const beta = 2;\n");
const { controllerRef, setFilesRef, setup } = await renderTerminalReview([
createAlpha(),
createBeta(),
]);

try {
await flush(setup);
await act(async () => {
expectValue(controllerRef.current).addLiveComment(
{ filePath: "beta.ts", side: "new", line: 1, summary: "Check beta rename" },
"comment-1",
{ reveal: false },
);
});
await flush(setup);
expect(expectValue(controllerRef.current).liveCommentCount).toBe(1);

await act(async () => {
expectValue(setFilesRef.current)([createAlpha()]);
});
await flush(setup);

expect(expectValue(controllerRef.current).liveCommentSummaries).toEqual([]);
expect(expectValue(controllerRef.current).liveCommentCount).toBe(0);

await act(async () => {
expectValue(setFilesRef.current)([createAlpha(), createBeta()]);
});
await flush(setup);

expect(
expectValue(controllerRef.current).liveCommentSummaries.map((comment) => comment.commentId),
).toEqual(["comment-1"]);
expect(expectValue(controllerRef.current).liveCommentCount).toBe(1);
} finally {
await act(async () => {
setup.renderer.destroy();
});
}
});

test("live comments validate markup at the published live width", async () => {
const noteGeometry: { current: { layout: "split" | "unified"; width: number } | null } = {
current: { layout: "unified", width: 120 },
Expand Down
6 changes: 4 additions & 2 deletions packages/hunk/src/ui/hooks/useTerminalReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1718,8 +1718,10 @@ export function useTerminalReview({
draftNote,
expandedGapsByFileId,
filter,
// Counted from the store, so notes on a file a reload retired still count as tracked.
liveCommentCount: state.liveNotes.length,
// Counted from the published summaries: the daemon refuses a snapshot whose count differs
// from its list. A note on a file a reload retired stays in the store and returns to both
// when a later reload brings the file back.
liveCommentCount: liveCommentSummaries.length,
liveCommentSummaries,
liveCommentsByFileId,
lineCursor,
Expand Down
75 changes: 75 additions & 0 deletions test/session/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,81 @@ sessionDescribe("session CLI integration", () => {
}
}, 20_000);

test("reload away from a noted file keeps the session and brings the note back with the file", async () => {
const port = await reserveLoopbackPort();
const fixture = createFixtureFiles(
"reload-retire",
["export const alpha = 1;"],
["export const alpha = 2;"],
);
mkdirSync(join(fixture.dir, ".git"));
const otherBefore = join(fixture.dir, "reload-other-before.ts");
const otherAfter = join(fixture.dir, "reload-other-after.ts");
writeFileSync(otherBefore, "export const other = 1;\n");
writeFileSync(otherAfter, "export const other = 2;\n");
const session = spawnHunkSession(fixture, port);

try {
const listed = await waitForRegisteredSessions(port);
const sessionId = listed[0]!.sessionId;

const comment = runSessionCli(
[
"comment",
"add",
sessionId,
"--file",
fixture.afterName,
"--new-line",
"1",
"--summary",
"Alpha note",
"--json",
],
port,
);
expect(comment.proc.exitCode).toBe(0);
expect(comment.stderr).toBe("");

// The new diff leaves out the noted file, so the note stays in the window's store only.
const away = runSessionCli(
["reload", sessionId, "--json", "--", "diff", "--files", otherBefore, otherAfter],
port,
);
expect(away.stderr).toBe("");
expect(away.proc.exitCode).toBe(0);

const listedAway = runSessionCli(["list", "--json"], port);
expect(listedAway.proc.exitCode).toBe(0);
expect(
(JSON.parse(listedAway.stdout) as SessionListJson).sessions.map((entry) => entry.sessionId),
).toEqual([sessionId]);

const back = runSessionCli(
["reload", sessionId, "--json", "--", "diff", "--files", fixture.before, fixture.after],
port,
);
expect(back.stderr).toBe("");
expect(back.proc.exitCode).toBe(0);

const returned = await waitUntil("note back on its file", () => {
const listedComments = runSessionCli(["comment", "list", sessionId, "--json"], port);
if (listedComments.proc.exitCode !== 0) {
return null;
}
const parsed = JSON.parse(listedComments.stdout) as {
comments?: Array<{ summary?: string; filePath?: string }>;
};
return parsed.comments?.some((entry) => entry.summary === "Alpha note") ? parsed : null;
});
expect(returned.comments).toMatchObject([
{ summary: "Alpha note", filePath: fixture.afterName },
]);
} finally {
await cleanupHunkSession(session, fixture, port);
}
}, 20_000);

test("reload refuses to read files outside the live session root", async () => {
const port = await reserveLoopbackPort();
const fixture = createFixtureFiles(
Expand Down