SPB Git

spb/polyllm

Public

E2E: session reuse, provider selectors; manifest build env; docs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 8, 2026) parent 5734499 Browse files

2 changed files +20 −7

modified e2e/full-flow.spec.ts +9 −7
@@ -1,5 +1,5 @@
1 import { test, expect } from "@playwright/test"; 1 import { test, expect } from "@playwright/test";
2−import { TEST_EMAIL, TEST_PASSWORD, lastLinkFromLog, login, noConsoleErrors, readEnv, sql } from "./helpers"; 2+import { TEST_EMAIL, TEST_PASSWORD, lastLinkFromLog, login, noConsoleErrors, readEnv, restoreSession, saveSession, sql } from "./helpers";
3 3
4 /** 4 /**
5 * Full workflow against the dev server (EMAIL_DRY_RUN=1 EMAIL_DRY_RUN_PRINT=1 so links land in the log): 5 * Full workflow against the dev server (EMAIL_DRY_RUN=1 EMAIL_DRY_RUN_PRINT=1 so links land in the log):
@@ -52,6 +52,7 @@ test.describe.serial("PolyLLM full flow", () => {
52 52
53 test("add + validate provider keys (real providers)", async ({ page }) => { 53 test("add + validate provider keys (real providers)", async ({ page }) => {
54 await login(page, email, password); 54 await login(page, email, password);
55+ await saveSession(page);
55 await page.goto("/app/settings/providers"); 56 await page.goto("/app/settings/providers");
56 const names: Record<string, RegExp> = { anthropic: /^Anthropic$/, openai: /^OpenAI$/, xai: /^xAI$/, gemini: /^Google Gemini$/ }; 57 const names: Record<string, RegExp> = { anthropic: /^Anthropic$/, openai: /^OpenAI$/, xai: /^xAI$/, gemini: /^Google Gemini$/ };
57 for (const [provider, envName] of [ 58 for (const [provider, envName] of [
@@ -80,7 +81,7 @@ test.describe.serial("PolyLLM full flow", () => {
80 }); 81 });
81 82
82 test("select a model, send a message and receive a real stream", async ({ page }) => { 83 test("select a model, send a message and receive a real stream", async ({ page }) => {
83− await login(page, email, password); 84+ await restoreSession(page);
84 await page.goto("/app/chat"); 85 await page.goto("/app/chat");
85 await page.getByRole("button", { name: /select model/i }).click(); 86 await page.getByRole("button", { name: /select model/i }).click();
86 await page.getByLabel(/search models/i).fill("haiku 4.5"); 87 await page.getByLabel(/search models/i).fill("haiku 4.5");
@@ -98,7 +99,7 @@ test.describe.serial("PolyLLM full flow", () => {
98 }); 99 });
99 100
100 test("conversation persists after reload and a second model works", async ({ page }) => { 101 test("conversation persists after reload and a second model works", async ({ page }) => {
101− await login(page, email, password); 102+ await restoreSession(page);
102 await page.goto(conversationUrl); 103 await page.goto(conversationUrl);
103 await expect(page.getByText(/PONG/).first()).toBeVisible(); 104 await expect(page.getByText(/PONG/).first()).toBeVisible();
104 // switch to an OpenAI model in the same conversation and continue 105 // switch to an OpenAI model in the same conversation and continue
@@ -114,7 +115,7 @@ test.describe.serial("PolyLLM full flow", () => {
114 }); 115 });
115 116
116 test("model configuration is capability-driven", async ({ page }) => { 117 test("model configuration is capability-driven", async ({ page }) => {
117− await login(page, email, password); 118+ await restoreSession(page);
118 await page.goto("/app/chat"); 119 await page.goto("/app/chat");
119 await page.getByRole("button", { name: /select model/i }).click(); 120 await page.getByRole("button", { name: /select model/i }).click();
120 await page.getByLabel(/search models/i).fill("gpt-5.5"); 121 await page.getByLabel(/search models/i).fill("gpt-5.5");
@@ -124,19 +125,20 @@ test.describe.serial("PolyLLM full flow", () => {
124 await expect(page.getByText(/^Effort$/)).toBeVisible(); 125 await expect(page.getByText(/^Effort$/)).toBeVisible();
125 // seed/stop are not supported by the Responses API → never shown 126 // seed/stop are not supported by the Responses API → never shown
126 await expect(page.getByText(/^Seed$/)).toHaveCount(0); 127 await expect(page.getByText(/^Seed$/)).toHaveCount(0);
127− await expect(page.getByText(/stop sequences/i)).toHaveCount(0); 128+ await expect(page.getByText(/^Stop sequences$/)).toHaveCount(0);
129+ await expect(page.getByText(/not available for this model/i)).toContainText(/seed/i);
128 await page.keyboard.press("Escape"); 130 await page.keyboard.press("Escape");
129 }); 131 });
130 132
131 test("usage dashboard shows the requests", async ({ page }) => { 133 test("usage dashboard shows the requests", async ({ page }) => {
132− await login(page, email, password); 134+ await restoreSession(page);
133 await page.goto("/app/usage"); 135 await page.goto("/app/usage");
134 await expect(page.getByText(/requests/i).first()).toBeVisible(); 136 await expect(page.getByText(/requests/i).first()).toBeVisible();
135 await expect.poll(() => Number(sql(`select count(*) from usage_records ur join users u on u.id=ur.user_id where u.email='${email}'`))).toBeGreaterThanOrEqual(2); 137 await expect.poll(() => Number(sql(`select count(*) from usage_records ur join users u on u.id=ur.user_id where u.email='${email}'`))).toBeGreaterThanOrEqual(2);
136 }); 138 });
137 139
138 test("change password, logout, login again", async ({ page }) => { 140 test("change password, logout, login again", async ({ page }) => {
139− await login(page, email, password); 141+ await restoreSession(page);
140 await page.goto("/app/settings/security"); 142 await page.goto("/app/settings/security");
141 const newPassword = `${password}-2`; 143 const newPassword = `${password}-2`;
142 await page.getByLabel(/current password/i).fill(password); 144 await page.getByLabel(/current password/i).fill(password);
modified e2e/helpers.ts +11 −0
@@ -42,3 +42,14 @@ export async function noConsoleErrors(page: Page) {
42 page.on("pageerror", (e) => errors.push(e.message)); 42 page.on("pageerror", (e) => errors.push(e.message));
43 return errors; 43 return errors;
44 } 44 }
45+
46+const STATE_FILE = "test-results/.e2e-session.json";
47+/** Persist the signed-in cookies so later tests don't hit the sign-in rate limit (8/min). */
48+export async function saveSession(page: Page) {
49+ fs.mkdirSync("test-results", { recursive: true });
50+ await page.context().storageState({ path: STATE_FILE });
51+}
52+export async function restoreSession(page: Page) {
53+ const state = JSON.parse(fs.readFileSync(STATE_FILE, "utf8")) as { cookies: Parameters<Page["context"]>[0] extends never ? never : import("@playwright/test").Cookie[] };
54+ await page.context().addCookies(state.cookies);
55+}
45 56
Menu

Theme