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
155 changes: 155 additions & 0 deletions tests/web/app-render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,161 @@ describe("OpenPI React transcript", () => {
expect(container.querySelector("br")).toBeTruthy();
});

it("shows unavailable image evidence instead of calling an image-only result empty", () => {
const snapshot = activeSnapshot();
snapshot.runtime.status = "idle";
snapshot.selectedSession!.entries = [
{
type: "message",
id: "image-result",
timestamp: "2026-09-01T10:00:00Z",
message: {
role: "toolResult",
toolName: "future_tool",
toolCallId: "image-call",
content: "",
isError: false,
images: [{ mimeType: "image/png", bytes: 1 }],
imageCount: 1,
},
},
];

const { container } = renderWithI18n(
createElement(Transcript, {
snapshot,
liveMessages: [],
liveRunning: false,
livePhase: "idle",
liveRetry: null,
thinkingStarts: {},
thinkingDurations: {},
scrollToBottom: 0,
onResend: async () => true,
}),
);

expect(screen.queryByText(i18n.t("noOutput"))).toBeNull();
expect(screen.getByText(i18n.t("toolImageUnavailable"))).toBeTruthy();
expect(screen.getByText(/image\/png/u)).toBeTruthy();
expect(container.querySelector("[aria-label=completed]")).toBeTruthy();
});

it("labels clipped image content without claiming unseen image counts", () => {
const snapshot = activeSnapshot();
snapshot.runtime.status = "idle";
snapshot.selectedSession!.entries = [
{
type: "message",
id: "clipped-result",
timestamp: "2026-09-01T10:00:00Z",
message: {
role: "toolResult",
toolName: "future_tool",
toolCallId: "clipped-call",
content: "",
isError: false,
truncation: { truncated: true, partsOmitted: 1 },
},
},
];

const { container } = renderWithI18n(
createElement(Transcript, {
snapshot,
liveMessages: [],
liveRunning: false,
livePhase: "idle",
liveRetry: null,
thinkingStarts: {},
thinkingDurations: {},
scrollToBottom: 0,
onResend: async () => true,
}),
);

expect(screen.queryByText(i18n.t("noOutput"))).toBeNull();
expect(
screen.getByText(i18n.t("toolContentPartsOmitted", { count: 1 })),
).toBeTruthy();
expect(container.querySelector("[aria-label=completed]")).toBeTruthy();
});

it("keeps image evidence in an unpaired activity result", () => {
const snapshot = activeSnapshot();
snapshot.runtime.status = "idle";
snapshot.selectedSession!.entries = [
{
type: "message",
id: "activity-image-result",
timestamp: "2026-09-01T10:00:00Z",
message: {
role: "toolResult",
toolName: "workflow_run",
content: "",
isError: false,
images: [{ mimeType: "image/png", bytes: 1 }],
imageCount: 1,
},
},
];

renderWithI18n(
createElement(Transcript, {
snapshot,
liveMessages: [],
liveRunning: false,
livePhase: "idle",
liveRetry: null,
thinkingStarts: {},
thinkingDurations: {},
scrollToBottom: 0,
onResend: async () => true,
}),
);

expect(screen.getByText(i18n.t("toolImageUnavailable"))).toBeTruthy();
expect(screen.queryByText(i18n.t("noOutput"))).toBeNull();
});

it("keeps image evidence when the tool result is an error", () => {
const snapshot = activeSnapshot();
snapshot.runtime.status = "idle";
snapshot.selectedSession!.entries = [
{
type: "message",
id: "failed-image-result",
timestamp: "2026-09-01T10:00:00Z",
message: {
role: "toolResult",
toolName: "future_tool",
content: "",
isError: true,
images: [{ mimeType: "image/png", bytes: 1 }],
imageCount: 1,
},
},
];

const { container } = renderWithI18n(
createElement(Transcript, {
snapshot,
liveMessages: [],
liveRunning: false,
livePhase: "idle",
liveRetry: null,
thinkingStarts: {},
thinkingDurations: {},
scrollToBottom: 0,
onResend: async () => true,
}),
);

expect(screen.getByText(i18n.t("toolImageUnavailable"))).toBeTruthy();
expect(screen.queryByText(i18n.t("noOutput"))).toBeNull();
expect(container.querySelector("[aria-label=failed]")).toBeTruthy();
});

it("matches Session searches after preserving a trailing input space", () => {
const store = createWebStore();
store.getState().actions.setQuery("foo ");
Expand Down
52 changes: 52 additions & 0 deletions tests/web/evidence-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,58 @@ it("renders file context, exact diffs, TAP failures, terminal cancellation and s
).toHaveLength(4);
});

it("keeps image metadata visible in specialized read evidence", () => {
render(
createElement(ToolEvidence, {
call: {
type: "toolCall",
name: "read",
arguments: JSON.stringify({ path: "image.png" }),
},
result: {
content: "",
isError: false,
images: [{ mimeType: "image/png", bytes: 1 }],
imageCount: 1,
},
}),
);

expect(
screen.getByText("1 image included; preview unavailable in Web.", {
exact: true,
}),
).toBeTruthy();
expect(screen.getByText(/image\/png/u)).toBeTruthy();
});

it("keeps text output alongside image metadata in specialized read evidence", () => {
render(
createElement(ToolEvidence, {
call: {
type: "toolCall",
name: "read",
arguments: JSON.stringify({ path: "image.png" }),
},
result: {
content: "caption",
isError: false,
images: [{ mimeType: "image/png", bytes: 1 }],
imageCount: 1,
},
}),
);

expect(
screen.getByRole("figure", { name: "File content" }).textContent,
).toContain("caption");
expect(
screen.getByText("1 image included; preview unavailable in Web.", {
exact: true,
}),
).toBeTruthy();
});

it("opens local links using authenticated API and stops preview reads on close", async () => {
const resolve = vi
.spyOn(WebClient.prototype, "resolveArtifact")
Expand Down
12 changes: 11 additions & 1 deletion tests/web/evidence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,21 @@ test("tool updates replace snapshots, final results resist replay, caches are bo
result: { content: "ab" },
});
assert.equal(tools[0]?.result?.content, "ab");
const imageResult = projectMessage({
role: "toolResult",
content: [{ type: "image", data: "AA==", mimeType: "image/png" }],
isError: false,
});
tools = reduceLiveTools(tools, "tool_execution_end", {
toolCallId: "call",
result: { content: "final", isError: false },
result: imageResult,
isError: false,
});
assert.equal(tools[0]?.result?.content, "");
assert.equal(tools[0]?.result?.imageCount, 1);
assert.deepEqual(tools[0]?.result?.images, [
{ mimeType: "image/png", bytes: 1 },
]);
const replayed = reduceLiveTools(tools, "tool_execution_update", {
call: bash,
toolCallId: "call",
Expand Down
86 changes: 86 additions & 0 deletions tests/web/openpi-web.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,92 @@ test("production workbench is local, keyboard-operable, and accessible", async (
);
});

test("keeps an image-only tool result visible across snapshot reload", async ({
page,
}, testInfo) => {
await page.route("**/api/snapshot**", async (route) => {
const response = await route.fetch();
const snapshot = await response.json();
snapshot.currentSessionId = "image-result-session";
snapshot.selectedSession = {
id: "image-result-session",
path: "/image-result/session.jsonl",
cwd: "/image-result",
entries: [
{
id: "image-assistant",
type: "message",
timestamp: "2026-09-13T00:00:00Z",
message: {
role: "assistant",
content: "",
parts: [
{
type: "toolCall",
id: "image-call",
name: "read",
arguments: JSON.stringify({ path: "image.png" }),
},
],
},
},
{
id: "image-tool-result",
type: "message",
timestamp: "2026-09-13T00:00:01Z",
message: {
role: "toolResult",
toolName: "read",
toolCallId: "image-call",
content: "",
isError: false,
images: [{ mimeType: "image/png", bytes: 1 }],
imageCount: 1,
},
},
],
bytes: 300,
truncation: {
truncated: false,
maxBytes: 2_097_152,
entriesOmitted: 0,
messagesTruncated: 0,
messagePartsOmitted: 0,
},
};
snapshot.runtime = { status: "idle", capabilities: {} };
await route.fulfill({ response, json: snapshot });
});
await page.route("**/events?**", (route) =>
route.fulfill({
status: 200,
contentType: "text/event-stream",
body: ": image fixture idle\n\n",
}),
);

const openResultEvidence = async () => {
await expect(page.locator(".tool-evidence-card")).toHaveCount(1);
await page.locator(".tool-evidence-card > summary").click();
await expect(
page.getByText("结果包含 1 张图片,Web 暂不提供图片预览。", {
exact: true,
}),
).toBeVisible();
await expect(page.getByText("无输出", { exact: true })).toHaveCount(0);
};

await openWorkbench(page);
await openResultEvidence();
await page.screenshot({
path: testInfo.outputPath("image-only-result.png"),
fullPage: true,
});
await page.reload();
await openWorkbench(page);
await openResultEvidence();
});

test("discovers and completes Pi commands without submitting unsupported commands", async ({
page,
}) => {
Expand Down
Loading
Loading