Files
paseo/packages/app/src/utils/pending-create-images.test.ts
T
Mohamed BoudraandGitHub 92afb826d9 Add inline review comments to git diff pane (#530)
* Add inline git diff review comments

Adds a review draft store keyed per diff target, inline gutter + thread
rendering in the diff pane, and a generated review attachment that flows
through the composer and agent providers.

* Strip generated review attachments before saving to draft store

Drafts persist only UserComposerAttachment (no review kind), so filter the
auto-generated review attachment out when seeding the draft or restoring
after a failed auto-submit.

* Fix inline review typecheck and test harness drift

* Unslop inline review feature

- Fix stale-closure bug in composer queueMessage: clear drafts for the
  actually-queued attachments, not the latest props
- Drop dead useEffect mirroring initialBody in inline review editor
  (parent already remounts via key)
- Restore typed AgentAttachment signature on renderPromptAttachmentAsText;
  delete schema redispatch helper and drop null-checks across 5 providers
- Inline single-use review prompt formatters and replace marker switch
  with const map
- Drop normalizeComment field-copy redundancy after type guard
- Remove __reviewDraftStoreTestUtils export; test migration via
  rehydrate() public path
- Refactor findTarget to return the target line directly (no non-null
  assertion at call site)
- Drop redundant isGeneratedReviewAttachment helper; inline kind check

* Update review pill to withUnistyles after rebase

Main's unistyles refactor stripped useUnistyles in favor of withUnistyles +
StyleSheet. Convert ReviewAttachmentPill to use ThemedCircleDot + ICON_SIZE so
the icon picks up the foreground-muted mapping like the other pills.

* Fix lint and typecheck after rebase onto main

Resolve drift after main's withUnistyles refactor:
- Composer complexity: dedup voice?.isVoiceSwitching to drop to 20
- Move per-comment Pressable handlers in git-diff-inline-review into
  CommentRow subcomponent so callbacks are stable
- Hoist style/onPress factories to module level (iconButton,
  iconButtonDestructive, ghostButton)
- Memoize style arrays in git-diff-pane review wrappers
- Replace inline {} placeholders with useMemo styles
- review-draft-store: extract applyCommentUpdates to avoid map-spread,
  include input.key in useMemo deps
- Test fixtures: hoist EMPTY_COMMENTS, add buildReviewActions helper

* Drop GitDiffPane default param to keep complexity at 20

After the rebase, main's `enabled = true` default param combined with the
inline review additions pushed cyclomatic complexity to 21. Drop the
default and resolve `enabled !== false` at the call site (comparison
doesn't count toward complexity).

* Reshape inline review behind @/review module

Drop the `generated: true` attachment flag and consolidate inline-review
logic behind a deep `@/review` module so composer and git-diff-pane stop
hand-composing review primitives. Workspace-scoped review snapshots are
now derived at submit/queue time rather than persisted in the draft
store. Composer review references drop from 51 to 4; git-diff-pane drops
~108 lines and now consumes a controller hook + slot components.

* Fix optimistic review attachments
2026-04-30 21:35:58 +08:00

155 lines
4.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { StreamItem } from "@/types/stream";
import { mergePendingCreateImages } from "./pending-create-images";
import type { AgentAttachment } from "@server/shared/messages";
function userMessage(params: {
id: string;
text: string;
images?: Array<{
id: string;
storageType: "native-file";
storageKey: string;
mimeType: string;
createdAt: number;
}>;
attachments?: AgentAttachment[];
}): StreamItem {
return {
kind: "user_message",
id: params.id,
text: params.text,
timestamp: new Date("2026-01-01T00:00:00Z"),
...(params.images ? { images: params.images } : {}),
...(params.attachments ? { attachments: params.attachments } : {}),
};
}
function buildImage(id: string) {
return [
{
id,
storageType: "native-file" as const,
storageKey: `/tmp/${id}.jpg`,
mimeType: "image/jpeg",
createdAt: Date.now(),
},
];
}
function buildReviewAttachment(): AgentAttachment {
return {
type: "review",
mimeType: "application/paseo-review",
cwd: "/repo",
mode: "base",
comments: [],
};
}
describe("mergePendingCreateImages", () => {
it("returns same reference when pending images are absent", () => {
const streamItems = [userMessage({ id: "msg-1", text: "hello" })];
const result = mergePendingCreateImages({
streamItems,
clientMessageId: "msg-1",
images: [],
});
expect(result).toBe(streamItems);
});
it("merges images by clientMessageId when matched message has none", () => {
const streamItems = [userMessage({ id: "msg-1", text: "hello" })];
const images = buildImage("image-1");
const result = mergePendingCreateImages({
streamItems,
clientMessageId: "msg-1",
images,
});
expect(result).not.toBe(streamItems);
const updated = result[0];
expect(updated?.kind).toBe("user_message");
if (updated?.kind !== "user_message") {
throw new Error("Expected user_message item");
}
expect(updated.images).toEqual(images);
});
it("merges attachments by clientMessageId when matched message has none", () => {
const streamItems = [userMessage({ id: "msg-1", text: "hello" })];
const attachments = [buildReviewAttachment()];
const result = mergePendingCreateImages({
streamItems,
clientMessageId: "msg-1",
attachments,
});
expect(result).not.toBe(streamItems);
const updated = result[0];
expect(updated?.kind).toBe("user_message");
if (updated?.kind !== "user_message") {
throw new Error("Expected user_message item");
}
expect(updated.attachments).toEqual(attachments);
});
it("does not merge when clientMessageId does not match", () => {
const streamItems = [userMessage({ id: "msg-1", text: "same text" })];
const images = buildImage("image-2");
const result = mergePendingCreateImages({
streamItems,
clientMessageId: "missing-id",
images,
});
expect(result).toBe(streamItems);
});
it("does not overwrite existing user message images", () => {
const existingImages = buildImage("existing");
const streamItems = [userMessage({ id: "msg-1", text: "hello", images: existingImages })];
const result = mergePendingCreateImages({
streamItems,
clientMessageId: "msg-1",
images: buildImage("new"),
});
expect(result).toBe(streamItems);
const unchanged = result[0];
expect(unchanged?.kind).toBe("user_message");
if (unchanged?.kind !== "user_message") {
throw new Error("Expected user_message item");
}
expect(unchanged.images).toEqual(existingImages);
});
it("does not overwrite existing user message attachments", () => {
const existingAttachments = [buildReviewAttachment()];
const streamItems = [
userMessage({ id: "msg-1", text: "hello", attachments: existingAttachments }),
];
const result = mergePendingCreateImages({
streamItems,
clientMessageId: "msg-1",
attachments: [
{
type: "github_issue",
mimeType: "application/github-issue",
number: 7,
title: "Issue",
url: "https://github.com/getpaseo/paseo/issues/7",
},
],
});
expect(result).toBe(streamItems);
const unchanged = result[0];
expect(unchanged?.kind).toBe("user_message");
if (unchanged?.kind !== "user_message") {
throw new Error("Expected user_message item");
}
expect(unchanged.attachments).toEqual(existingAttachments);
});
});