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
206 changes: 205 additions & 1 deletion apps/web/src/components/ChatView.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import "../index.css";

import {
CheckpointRef,
ORCHESTRATION_WS_METHODS,
type MessageId,
type OrchestrationReadModel,
type ProjectId,
type ServerConfig,
type ThreadId,
type TurnId,
type WsWelcomePayload,
WS_CHANNELS,
WS_METHODS,
Expand Down Expand Up @@ -279,6 +281,85 @@ function createSnapshotForTargetUser(options: {
};
}

function createSnapshotWithTurnDiffCheckpoints(): OrchestrationReadModel {
const snapshot = createSnapshotForTargetUser({
targetMessageId: "msg-user-diff-panel-latest-chip-test" as MessageId,
targetText: "diff panel latest chip test",
});
const threads = [...snapshot.threads];
const threadIndex = threads.findIndex((thread) => thread.id === THREAD_ID);

if (threadIndex < 0) {
return snapshot;
}

const thread = threads[threadIndex];
if (!thread) {
return snapshot;
}

threads[threadIndex] = {
...thread,
checkpoints: [
{
turnId: "turn-1" as TurnId,
checkpointTurnCount: 1,
checkpointRef: CheckpointRef.makeUnsafe("refs/t3/checkpoints/checkpoint-1"),
status: "ready" as const,
files: [],
assistantMessageId: null,
completedAt: isoAt(120),
},
{
turnId: "turn-2" as TurnId,
checkpointTurnCount: 2,
checkpointRef: CheckpointRef.makeUnsafe("refs/t3/checkpoints/checkpoint-2"),
status: "ready" as const,
files: [],
assistantMessageId: null,
completedAt: isoAt(240),
},
],
};

return {
...snapshot,
threads,
};
}

function appendCheckpointToStore(
state: ReturnType<typeof useStore.getState>,
checkpoint: {
turnId: TurnId;
checkpointTurnCount: number;
checkpointRef: string;
completedAt: string;
},
): ReturnType<typeof useStore.getState> {
return {
...state,
threads: state.threads.map((thread) =>
thread.id === THREAD_ID
? {
...thread,
turnDiffSummaries: [
...thread.turnDiffSummaries,
{
turnId: checkpoint.turnId,
checkpointTurnCount: checkpoint.checkpointTurnCount,
checkpointRef: CheckpointRef.makeUnsafe(checkpoint.checkpointRef),
status: "ready" as const,
files: [],
completedAt: checkpoint.completedAt,
},
],
}
: thread,
),
};
}

function buildFixture(snapshot: OrchestrationReadModel): TestFixture {
return {
snapshot,
Expand Down Expand Up @@ -607,6 +688,22 @@ async function waitForURL(
return pathname;
}

async function waitForSearch(
router: ReturnType<typeof getRouter>,
predicate: (search: Record<string, unknown>) => boolean,
errorMessage: string,
): Promise<Record<string, unknown>> {
let search: Record<string, unknown> = {};
await vi.waitFor(
() => {
search = router.state.location.search as Record<string, unknown>;
expect(predicate(search), errorMessage).toBe(true);
},
{ timeout: 8_000, interval: 16 },
);
return search;
}

async function waitForComposerEditor(): Promise<HTMLElement> {
return waitForElement(
() => document.querySelector<HTMLElement>('[contenteditable="true"]'),
Expand Down Expand Up @@ -774,6 +871,7 @@ async function mountChatView(options: {
viewport: ViewportSpec;
snapshot: OrchestrationReadModel;
configureFixture?: (fixture: TestFixture) => void;
initialEntry?: string;
resolveRpc?: (body: WsRequestEnvelope["body"]) => unknown | undefined;
}): Promise<MountedChatView> {
fixture = buildFixture(options.snapshot);
Expand All @@ -793,7 +891,7 @@ async function mountChatView(options: {

const router = getRouter(
createMemoryHistory({
initialEntries: [`/${THREAD_ID}`],
initialEntries: [options.initialEntry ?? `/${THREAD_ID}`],
}),
);

Expand Down Expand Up @@ -884,6 +982,112 @@ describe("ChatView timeline estimator parity (full app)", () => {
document.body.innerHTML = "";
});

it("selects the newest checkpoint when the Latest diff chip is clicked", async () => {
const mounted = await mountChatView({
viewport: DEFAULT_VIEWPORT,
snapshot: createSnapshotWithTurnDiffCheckpoints(),
initialEntry: `/${THREAD_ID}?diff=1`,
});

try {
const latestTurnButton = await waitForElement(
() =>
Array.from(document.querySelectorAll("button")).find(
(button) => button.textContent?.trim() === "Latest",
) as HTMLButtonElement | null,
"Unable to find the Latest diff chip.",
);

await waitForElement(
() =>
Array.from(document.querySelectorAll("button")).find((button) =>
button.textContent?.includes("Turn 2"),
) as HTMLButtonElement | null,
"Unable to find the grouped latest turn chip.",
);

expect(mounted.router.state.location.search.diffSelection).toBeUndefined();
expect(mounted.router.state.location.search.diffTurnId).toBeUndefined();

latestTurnButton.click();

await waitForSearch(
mounted.router,
(search) =>
search.diff === "1" &&
search.diffSelection === "latest" &&
search.diffTurnId === undefined,
"Latest diff chip should switch the diff panel into follow-latest mode.",
);

expect(
document.querySelectorAll("[data-turn-chip-selected='true']"),
"Latest mode should only mark a single chip as selected.",
).toHaveLength(1);
} finally {
await mounted.cleanup();
}
});

it("keeps following the newest checkpoint while Latest mode is selected", async () => {
const mounted = await mountChatView({
viewport: DEFAULT_VIEWPORT,
snapshot: createSnapshotWithTurnDiffCheckpoints(),
initialEntry: `/${THREAD_ID}?diff=1&diffSelection=latest`,
});

try {
await waitForElement(
() =>
Array.from(document.querySelectorAll("button")).find(
(button) => button.textContent?.trim() === "Latest",
) as HTMLButtonElement | null,
"Unable to find the initial Latest diff chip state.",
);

await waitForElement(
() =>
Array.from(document.querySelectorAll("button")).find((button) =>
button.textContent?.includes("Turn 2"),
) as HTMLButtonElement | null,
"Unable to find the initial grouped latest turn chip.",
);

useStore.setState((state) =>
appendCheckpointToStore(state, {
turnId: "turn-3" as TurnId,
checkpointTurnCount: 3,
checkpointRef: "refs/t3/checkpoints/checkpoint-3",
completedAt: isoAt(360),
}),
);

await waitForElement(
() =>
Array.from(document.querySelectorAll("button")).find((button) =>
button.textContent?.includes("Turn 3"),
) as HTMLButtonElement | null,
"Latest diff chip should update to the newest checkpoint turn.",
);

await waitForSearch(
mounted.router,
(search) =>
search.diff === "1" &&
search.diffSelection === "latest" &&
search.diffTurnId === undefined,
"Latest mode should keep routing state in follow-latest mode after new checkpoints arrive.",
);

expect(
document.querySelectorAll("[data-turn-chip-selected='true']"),
"Latest mode should still have exactly one selected chip after new checkpoints arrive.",
).toHaveLength(1);
} finally {
await mounted.cleanup();
}
});

it.each(TEXT_VIEWPORT_MATRIX)(
"keeps long user message estimate close at the $name viewport",
async (viewport) => {
Expand Down
Loading
Loading