A run of the real prompt against the local model showed reasoning written as chat text, internal tool names in a capabilities answer, and only a data fetch when asked to build a tool. The working principles now say that everything written is shown, to describe abilities in plain words, and that a request for a tool means building one. The chart-links spec now waits for the chart or its error and reports browser errors, so the recurring 90-second timeout names its cause. The ZIP upload spec matches the file chip exactly, since the agent reply can quote the name.
113 lines
4.8 KiB
TypeScript
113 lines
4.8 KiB
TypeScript
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { expect, test } from "@playwright/test";
|
|
import { captureScreenshot, completeOnboarding, signup } from "./helpers";
|
|
|
|
test.describe.configure({ mode: "serial" });
|
|
|
|
// VP9 in MP4: Playwright's Chromium has no H.264 decoder, real browsers play both.
|
|
const clip = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures", "media-clip.mp4");
|
|
const pixel = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
|
|
"base64",
|
|
);
|
|
|
|
test("an uploaded video streams from the artifact route and plays inline", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const stamp = Date.now();
|
|
await signup(page, `media-video-${stamp}@rakazo.test`, "password12", "Media Video");
|
|
await completeOnboarding(page);
|
|
|
|
const upload = page.waitForResponse(
|
|
(response) =>
|
|
response.request().method() === "POST" && /\/api\/artifacts\?/.test(response.url()),
|
|
);
|
|
await page.locator('input[type="file"]').setInputFiles(clip);
|
|
await expect(page.getByRole("button", { name: "Remove media-clip.mp4" })).toBeVisible();
|
|
await page.getByRole("combobox", { name: /Message/ }).fill("here is the clip");
|
|
await page.keyboard.press("Enter");
|
|
expect((await upload).status()).toBe(201);
|
|
|
|
const video = page.getByTestId("media-video").locator("video");
|
|
await expect(video).toBeVisible({ timeout: 30_000 });
|
|
await expect(video).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/);
|
|
await expect
|
|
.poll(() => video.evaluate((element: HTMLVideoElement) => element.readyState), {
|
|
timeout: 15_000,
|
|
})
|
|
.toBeGreaterThanOrEqual(1);
|
|
expect(await video.evaluate((element: HTMLVideoElement) => element.duration)).toBeCloseTo(1, 0);
|
|
|
|
await video.evaluate(async (element: HTMLVideoElement) => {
|
|
element.muted = true;
|
|
await element.play();
|
|
});
|
|
await expect
|
|
.poll(() => video.evaluate((element: HTMLVideoElement) => element.currentTime))
|
|
.toBeGreaterThan(0);
|
|
await video.evaluate((element: HTMLVideoElement) => {
|
|
element.pause();
|
|
element.currentTime = 0.5;
|
|
element.scrollIntoView({ block: "center" });
|
|
});
|
|
await captureScreenshot(page, testInfo, "media-video-block");
|
|
});
|
|
|
|
test("an image in the main conversation opens and downloads from the artifact route", async ({
|
|
page,
|
|
}) => {
|
|
const stamp = Date.now();
|
|
await signup(page, `media-image-${stamp}@rakazo.test`, "password12", "Media Image");
|
|
// The main conversation is a group thread, so this covers group uploads and reads.
|
|
await completeOnboarding(page, undefined, { mainConversation: true });
|
|
|
|
await page
|
|
.locator('input[type="file"]')
|
|
.setInputFiles({ name: "pixel.png", mimeType: "image/png", buffer: pixel });
|
|
await page.getByRole("combobox", { name: /Message/ }).fill("a pixel");
|
|
await page.keyboard.press("Enter");
|
|
|
|
const thumbnail = page.getByTestId("transcript").getByRole("img", { name: "pixel.png" });
|
|
await expect(thumbnail).toBeVisible({ timeout: 30_000 });
|
|
await expect(thumbnail).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/);
|
|
await thumbnail.click();
|
|
|
|
const dialog = page.getByRole("dialog", { name: "pixel.png" });
|
|
const download = dialog.getByRole("link", { name: "Download pixel.png" });
|
|
await expect(download).toHaveAttribute("href", /\/api\/artifacts\/[^/?]+\?download=1$/);
|
|
const saved = page.waitForEvent("download");
|
|
await download.click();
|
|
expect((await saved).suggestedFilename()).toBe("pixel.png");
|
|
});
|
|
|
|
test("a repository ZIP above the inline cap streams up as a file", async ({ page }, testInfo) => {
|
|
const stamp = Date.now();
|
|
await signup(page, `zip-upload-${stamp}@rakazo.test`, "password12", "Zip Upload");
|
|
await completeOnboarding(page);
|
|
|
|
const input = page.locator('input[type="file"]');
|
|
await expect(input).toHaveAttribute("accept", /application\/zip/);
|
|
const upload = page.waitForResponse(
|
|
(response) =>
|
|
response.request().method() === "POST" && /\/api\/artifacts\?/.test(response.url()),
|
|
);
|
|
// Windows reports ZIPs under the legacy type; 12 MiB is above the 10 MiB inline cap.
|
|
await input.setInputFiles({
|
|
name: "repo.zip",
|
|
mimeType: "application/x-zip-compressed",
|
|
buffer: Buffer.alloc(12 * 1024 * 1024, 1),
|
|
});
|
|
await expect(page.getByRole("button", { name: "Remove repo.zip" })).toBeVisible();
|
|
await page.getByRole("combobox", { name: /Message/ }).fill("here is the repo");
|
|
await page.keyboard.press("Enter");
|
|
const response = await upload;
|
|
expect(response.status()).toBe(201);
|
|
expect(response.request().headers()["content-type"]).toBe("application/zip");
|
|
// Exact: the agent's reply can quote the file name too.
|
|
await expect(page.getByTestId("transcript").getByText("repo.zip", { exact: true })).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await captureScreenshot(page, testInfo, "zip-upload");
|
|
});
|