Files
kandev/apps/web/e2e/tests/task/mobile-plan-size-ceiling.spec.ts
T
Clément Fiere 2a29104412 fix: harden prompt history and saved prompt aliases (#3369)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
Co-authored-by: Henry <nova28@users.noreply.github.com>
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: João Salavisa <joao.salavisa@gmail.com>
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-09-06 11:34:48 +01:00

115 lines
4.4 KiB
TypeScript

// Filename starts with "mobile-" so this runs under the mobile-chrome project.
import { test, expect } from "../../fixtures/test-base";
import { SessionPage } from "../../pages/session-page";
import { dwell, watchWs } from "../../helpers/causal-waits";
import { attachGatewayTrafficCapture } from "../../helpers/ws-traffic";
import { formatBytes } from "../../../lib/utils/format-bytes";
const CEILING_BYTES = 262_144;
const SUBMITTED_BYTES = 300_000;
const MARKER = "MOBILE-PLAN-SIZE-CEILING-MARKER";
function contentOfByteLength(totalBytes: number): string {
const markerBytes = new TextEncoder().encode(MARKER).byteLength;
const fillerBytes = totalBytes - markerBytes;
return `${MARKER}${"€".repeat(Math.ceil(fillerBytes / 3))}`;
}
async function pasteIntoPlanEditor(session: SessionPage, content: string) {
const editor = session.planEditor();
// Dispatch a browser paste event so ProseMirror processes the text through
// its normal clipboard path.
await editor.evaluate((element, pasted) => {
element.focus();
const clipboardData = new DataTransfer();
clipboardData.setData("text/plain", pasted);
const pasteEvent = new ClipboardEvent("paste", {
bubbles: true,
cancelable: true,
clipboardData,
});
element.dispatchEvent(pasteEvent);
}, content);
}
async function expectSizeRejection(pending: Promise<unknown>) {
try {
await pending;
throw new Error("expected the mobile plan write to be rejected for content size");
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
expect(message).toContain("plan_content_too_large");
const match = message.match(/"limit":(\d+).*"submitted":(\d+)/);
if (!match) throw new Error(`could not parse size details from rejection: ${message}`);
return { limit: Number(match[1]), submitted: Number(match[2]) };
}
}
test.describe("mobile: plan content size ceiling", () => {
test("touch plan entry keeps a rejected draft and recovers after shortening", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
const wsWatcher = watchWs(testPage);
const traffic = attachGatewayTrafficCapture(testPage);
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Mobile plan size ceiling",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.waitForChatIdle({ timeout: 45_000 });
await session.togglePlanMode();
await testPage.getByRole("navigation").getByRole("button", { name: "Plan", exact: true }).tap();
await expect(session.planPanel).toBeVisible({ timeout: 10_000 });
const rejectedCreate = wsWatcher.waitForResponse("task.plan.create", { timeout: 30_000 });
await pasteIntoPlanEditor(session, contentOfByteLength(SUBMITTED_BYTES));
const rejection = await expectSizeRejection(rejectedCreate);
expect(rejection.limit).toBe(CEILING_BYTES);
expect(rejection.submitted).toBeGreaterThan(CEILING_BYTES);
const banner = session.planPanel.getByTestId("plan-save-error-banner");
await expect(banner).toBeVisible({ timeout: 10_000 });
await expect(banner).toContainText(formatBytes(rejection.limit));
await expect(banner).toContainText(formatBytes(rejection.submitted));
await expect(session.planEditor()).toContainText(MARKER);
await expect(await apiClient.getTaskPlan(task.id)).toBeNull();
await dwell(
testPage,
4_000,
"negative-assertion",
"confirms mobile autosave does not resubmit a size-rejected draft",
);
expect(
traffic.frames.filter(
(frame) => frame.direction === "sent" && frame.action === "task.plan.create",
),
).toHaveLength(1);
const acceptedCreate = wsWatcher.waitForResponse("task.plan.create", { timeout: 15_000 });
const shortContent = "Shortened mobile plan.";
await session.planEditor().click();
await session.planEditor().fill(shortContent);
await expect(acceptedCreate).resolves.toBeTruthy();
await expect(banner).toBeHidden({ timeout: 10_000 });
await expect
.poll(async () => (await apiClient.getTaskPlan(task.id))?.content, { timeout: 10_000 })
.toBe(shortContent);
});
});