Skip to content

Commit 00a22c6

Browse files
committed
fix(webapp): treat a transcript re-read that isn't a list as a failed read
1 parent c2f7c6f commit 00a22c6

2 files changed

Lines changed: 56 additions & 3 deletions

File tree

apps/webapp/app/components/dashboard-agent/settled-transcript.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { VIEW_BLOCK_VERSION } from "@internal/dashboard-agent-contracts";
2-
import { describe, expect, it } from "vitest";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
33
import { liveProgress } from "./progress-line";
44
import {
5+
fetchChatTranscript,
56
hasOpenInvestigation,
67
mergeSettledMessages,
78
pollSettledTranscript,
@@ -73,6 +74,51 @@ describe("merging a re-read transcript", () => {
7374
});
7475
});
7576

77+
describe("reading the transcript endpoint", () => {
78+
afterEach(() => {
79+
vi.unstubAllGlobals();
80+
});
81+
82+
function respondWith(body: unknown, ok = true) {
83+
vi.stubGlobal("fetch", async () => ({ ok, json: async () => body }) as unknown as Response);
84+
}
85+
86+
it("returns the transcript when the response carries one", async () => {
87+
respondWith({ messages: [OPEN, SETTLED] });
88+
const fetched = await fetchChatTranscript<typeof OPEN>("/agent/transcript", "chat_1");
89+
expect(fetched?.map((message) => message.id)).toEqual([OPEN.id, SETTLED.id]);
90+
});
91+
92+
it("reads a response with no messages at all as a failed re-read", async () => {
93+
respondWith({});
94+
expect(await fetchChatTranscript("/agent/transcript", "chat_1")).toBeNull();
95+
});
96+
97+
it("reads a non-array under messages as a failed re-read, not as a transcript", async () => {
98+
respondWith({ messages: { msg_open: OPEN } });
99+
expect(await fetchChatTranscript("/agent/transcript", "chat_1")).toBeNull();
100+
});
101+
102+
it("keeps only entries the merge can key on", async () => {
103+
respondWith({ messages: [OPEN, null, "msg_open", { revision: 1 }, SETTLED] });
104+
const fetched = await fetchChatTranscript<typeof OPEN>("/agent/transcript", "chat_1");
105+
expect(fetched?.map((message) => message.id)).toEqual([OPEN.id, SETTLED.id]);
106+
});
107+
108+
it("leaves the panel's transcript alone when the endpoint answers with a shape it cannot merge", async () => {
109+
respondWith({ messages: { msg_open: OPEN } });
110+
let rendered: (typeof OPEN)[] = [OPEN];
111+
112+
await pollSettledTranscript<typeof OPEN>({
113+
fetchTranscript: () => fetchChatTranscript("/agent/transcript", "chat_1"),
114+
apply: (merge) => void (rendered = merge(rendered)),
115+
wait: async () => {},
116+
});
117+
118+
expect(rendered).toEqual([OPEN]);
119+
});
120+
});
121+
76122
describe("an already-open panel when a turn is exhausted", () => {
77123
it("stops showing Working… without a reload or a reopen", async () => {
78124
// What the mounted panel holds when the stream closes: the card the model opened

apps/webapp/app/components/dashboard-agent/settled-transcript.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,15 @@ export async function fetchChatTranscript<T extends Identified>(
5959
try {
6060
const res = await fetch(`${actionPath}?chatId=${encodeURIComponent(chatId)}`);
6161
if (!res.ok) return null;
62-
const data = (await res.json()) as { messages?: T[] };
63-
return data.messages ?? null;
62+
const data = (await res.json()) as { messages?: unknown };
63+
if (!Array.isArray(data.messages)) return null;
64+
// Anything else under `messages` is not a transcript; keep only what merging can key on.
65+
return data.messages.filter(
66+
(message): message is T =>
67+
typeof message === "object" &&
68+
message !== null &&
69+
typeof (message as Identified).id === "string"
70+
);
6471
} catch (error) {
6572
console.error("Dashboard agent: failed to re-read the settled transcript", error);
6673
return null;

0 commit comments

Comments
 (0)