First-run gate offers free Ollama vs paid providers; add Google Gemini

The gate (user-key installs) now headlines Ollama with a "Free" badge and
adds an "or - Use a paid provider instead" option that opens the same
provider rows as Settings, framed as billed by the provider directly.
Saving a provider key there lists its models and switches study
generation to it automatically, so the gate steps aside with no Ollama
key at all.

Gemini joins the registry via its OpenAI-compatible endpoint
(generativelanguage.googleapis.com/v1beta/openai); the privacy policy
names it (en+cs).

Providers suite grows to 20 checks: the gate scenario runs against a
second user-key-mode app instance (free badge, paid option, provider
rows, pass-through, generation switch). All other gates green:
contrast 0 findings, mobile 126/126, chat 102/102, wizard 95/95,
bubbles 34/34, regression 44/44, i18n 13/13, native 33/33.
Version 0.4.9, SW -> freezmo-v73.
This commit is contained in:
Martin Dvořák 2026-10-14 07:09:05 +00:00
commit 8651674cf5
8 changed files with 132 additions and 8 deletions

View file

@ -113,6 +113,7 @@ const providerSeed = {
generation: { provider: "custom", textModel: "free-model-a", visionModel: "free-model-a" },
};
let gateApp = null;
try {
/* 1. API: the listing route forwards the key and returns the models ---- */
{
@ -211,6 +212,7 @@ try {
await page.locator("nav.tabbar").getByRole("button").click();
await page.getByRole("dialog").waitFor({ timeout: 6000 });
await page.waitForTimeout(600);
check("settings: Google Gemini is offered", (await page.locator('input[aria-label="Google Gemini API key"]').count()) === 1);
const row = page.locator('input[aria-label="Custom (OpenAI-compatible) API key"]');
await row.scrollIntoViewIfNeeded();
await row.fill(PROVIDER_KEY);
@ -224,8 +226,57 @@ try {
await context.close();
}
/* 5. First run: the gate offers the free Ollama path and paid providers - */
{
const GATE_PORT = 3214;
check("gate app port is free", !(await portInUse(GATE_PORT)));
// No OLLAMA_AUTH: the default user-key mode, so the first-run gate shows.
gateApp = spawnApp(GATE_PORT, {
OLLAMA_BASE_URL: `http://127.0.0.1:${STUB_PORT}`,
OLLAMA_VERIFY_KEY: "false",
});
await waitForHttp(`http://127.0.0.1:${GATE_PORT}/api/healthz`);
const { context, page } = await makePage(browser, { width: 390, height: 844 });
await seed(page, {
decks: [],
key: "",
extra: {
"freezmo.providers.v1": JSON.stringify({ keys: {}, models: {}, customBaseUrl: PROVIDER_BASE, generation: null }),
},
});
await page.goto(`http://127.0.0.1:${GATE_PORT}/`, { waitUntil: "networkidle" });
await page.waitForTimeout(1400);
check("gate: the free Ollama path is the headline", (await page.getByText("Free").count()) >= 1);
const paid = page.getByRole("button", { name: "Use a paid provider instead" });
check("gate: the paid option is visible on first run", await paid.isVisible());
await paid.click();
await page.waitForTimeout(500);
check("gate: the provider rows open", (await page.locator('input[aria-label="OpenAI API key"]').count()) === 1);
const gateKey = page.locator('input[aria-label="Custom (OpenAI-compatible) API key"]');
await gateKey.scrollIntoViewIfNeeded();
await gateKey.fill(PROVIDER_KEY);
await gateKey.locator("xpath=..").getByRole("button", { name: "Save key", exact: true }).click();
await page.waitForTimeout(2200);
check(
"gate: a saved provider key lets the app through",
await page.getByRole("heading", { name: "Your icebox" }).isVisible(),
);
check(
"gate: the provider became the study-generation model",
(await page.evaluate(() => {
const state = JSON.parse(localStorage.getItem("freezmo.providers.v1") ?? "{}");
return state.generation?.provider === "custom" && state.generation?.textModel === "free-model-a";
})),
);
await context.close();
}
check("stub saw both endpoints", seen.models >= 1 && seen.completions >= 1, JSON.stringify(seen));
} finally {
gateApp?.stop();
app.stop();
stub.close();
await browser.close();