Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .claude/skills/verify-debatelab/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: verify-debatelab
description: Launch and drive the real DebateLab web app (React client + local Supabase Auth/Postgres/Edge Functions, with either a fake model provider or real OpenRouter pinned to Claude Haiku 5.5 on the Anthropic BYOK key plus real Exa research) to prove a user-facing feature works, with screenshots, ARIA snapshots, provider calls, and database rows as evidence. Use when asked to verify, demo, reproduce, or screenshot DebateLab behavior in a browser, or before calling a DebateLab UI/Edge Function change done.
description: Launch and drive the real DebateOwl web app (React client + local Supabase Auth/Postgres/Edge Functions, with either a fake model provider or real OpenRouter pinned to Claude Haiku 5.5 on the Anthropic BYOK key plus real Exa research) to prove a user-facing feature works, with screenshots, ARIA snapshots, provider calls, and database rows as evidence. Use when asked to verify, demo, reproduce, or screenshot DebateOwl behavior in a browser, or before calling a DebateOwl UI/Edge Function change done.
---

# Verify DebateLab
# Verify DebateOwl

DebateLab is a web app. The surface a user touches is the browser client at `/login` and `/app/*`, backed by Supabase Auth, Postgres/RLS, and Deno Edge Functions. This skill runs all of that locally and drives it with Playwright. It has two modes:
DebateOwl is a web app. The surface a user touches is the browser client at `/login` and `/app/*`, backed by Supabase Auth, Postgres/RLS, and Deno Edge Functions. This skill runs all of that locally and drives it with Playwright. It has two modes:

- **Fake (default, free).** `e2e/full-stack/fake-openrouter.mjs` answers every model call with canned text. Proves wiring, rows, and UI, not model behavior.
- **Live (`up --live`, costs money).** The same container becomes a recording proxy to real OpenRouter. It rewrites every chat call to `anthropic/claude-haiku-5.5` pinned to the Anthropic provider with fallbacks off, so it bills the Anthropic BYOK key and not OpenRouter credits (override with `VERIFY_LIVE_MODEL`, which must be an `anthropic/` model), records OpenRouter's reply model as proof, and stops forwarding after `--max-calls` (default 400). Edge Functions also get a real Exa key, so web research and verified citations are real.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Preconditions:

## Gotchas

- A first sign-in on a new account starts the onboarding tour on `/app`. Its first step (`Welcome to DebateLab`, `Start your first debate here.`) offers `Start debate`. The fixtures pre-seed `debate-trainer-onboarding` to skip it. The tour is tracked per account (store version 1, `completedUserIds`). A version 0 seed with `hasCompletedOnboarding: true` covers only the next account that signs in, so a second account in the same browser sees the tour. Interactive runs click `Skip tour` (`[data-test-id="button-skip"]`). The tour itself is covered in [current-debate](./current-debate.md).
- A first sign-in on a new account starts the onboarding tour on `/app`. Its first step (`Welcome to DebateOwl`, `Start your first debate here.`) offers `Start debate`. The fixtures pre-seed `debate-trainer-onboarding` to skip it. The tour is tracked per account (store version 1, `completedUserIds`). A version 0 seed with `hasCompletedOnboarding: true` covers only the next account that signs in, so a second account in the same browser sees the tour. Interactive runs click `Skip tour` (`[data-test-id="button-skip"]`). The tour itself is covered in [current-debate](./current-debate.md).
- Sessions are stored under the `debate-auth-local` localStorage key in local-preview mode. Don't reuse stub-session helpers (`e2e/helpers/auth.ts`). They fake auth and prove rendering only.
- Google sign-in is not wired to the local stack. Demo codes, email codes and the email link work here.
- The revoked-session check forces one token refresh before it signs out. An Auth outage (network error, 5xx, 429) keeps the session, and that path is covered by `src/test/auth/session-expiry.test.ts`, not by a recipe.
4 changes: 2 additions & 2 deletions .claude/skills/verify-debatelab/features/localization.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Localization

The interface runs in English, Spanish and Hungarian. Every page names itself in the tab title in the interface language ("Generálás · DebateLab" in the app, "Felhasználási feltételek | DebateLab by Pi-Stack" on public pages), form validation and server refusals read in that language, the language switcher sits in every header, and `?lang=es|hu|en` links and the Hungarian and Spanish pages (`/hu`, `/es`, `/hu/tools/…`, `/es/tools/…`) open the app in that language.
The interface runs in English, Spanish and Hungarian. Every page names itself in the tab title in the interface language ("Generálás · DebateOwl" in the app, "Felhasználási feltételek | DebateOwl by Pi-Stack" on public pages), form validation and server refusals read in that language, the language switcher sits in every header, and `?lang=es|hu|en` links and the Hungarian and Spanish pages (`/hu`, `/es`, `/hu/tools/…`, `/es/tools/…`) open the app in that language.

## Sub-features

- loc-titles: `document.title` on app and public routes, per language, and a switch retitling the page without a reload. App routes read `<page> · DebateLab` (`usePrivateRouteHead`); public pages use the `pageHead` catalog section (`usePageHead`), whose English text is also what the prerender writes.
- loc-titles: `document.title` on app and public routes, per language, and a switch retitling the page without a reload. App routes read `<page> · DebateOwl` (`usePrivateRouteHead`); public pages use the `pageHead` catalog section (`usePageHead`), whose English text is also what the prerender writes.
- loc-validation: an empty Attack submit shows `validation.caseTextRequired` in the interface language, and nothing shows before the first submit. Generate, Brainstorm and Factoids use the same keys.
- loc-switcher: the switcher on `/login`, the 404 page, and the other public pages (`PublicPageShell`), besides the landing page and the app header.
- loc-links: `/?lang=hu` switches and saves the language, sets `<html lang>`, and drops the parameter; `/es` and `/hu` are prerendered pages of their own that keep their address, and Start from `/hu` leads into the app in Hungarian.
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/verify-debatelab/recipes/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ test("auth-deep-link-email-code: a signed-out deep link signs in by email code a
await page.goto("/app/generate");
await expect(page).toHaveURL(/\/login$/);
await expect(page.getByRole("main")).toHaveCount(1);
await expect(page.getByRole("heading", { level: 1, name: "Sign in to DebateLab" })).toBeVisible();
await expect(page.getByRole("heading", { level: 1, name: "Sign in to DebateOwl" })).toBeVisible();
await evidence.aria("01-login-from-deep-link");

let sentAfter = Date.now();
Expand Down Expand Up @@ -230,7 +230,7 @@ test("auth-switch-account: the next account on this browser starts on /app, gets
await evidence.step("Signed in as a second account; landed on /app, not /app/settings");

// AUTH-07: the second account gets its own tour.
const tour = page.getByText("Welcome to DebateLab");
const tour = page.getByText("Welcome to DebateOwl");
await expect(tour).toBeVisible({ timeout: 10_000 });
await evidence.shot("01-second-account-tour");
if (await page.getByRole("button", { name: "Open menu" }).isVisible()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ test("debate-tour: a brand-new account starts its first debate from the onboardi

const tooltip = page.locator(".react-joyride__tooltip");
const next = page.locator('[data-test-id="button-primary"]');
await expect(tooltip).toContainText("Welcome to DebateLab", { timeout: 15_000 });
await expect(tooltip).toContainText("Welcome to DebateOwl", { timeout: 15_000 });
await expect(tooltip).toContainText("Start your first debate here.");
await evidence.shot("01-tour-first-step");

Expand Down
16 changes: 8 additions & 8 deletions .claude/skills/verify-debatelab/recipes/localization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ async function chooseLanguage(page: Page, language: keyof typeof LANGUAGE_NAMES)
await expect(page.locator("html")).toHaveAttribute("lang", language);
}

/** The tab title once the page has set its own (the shell's is just "DebateLab"). */
/** The tab title once the page has set its own (the shell's is just "DebateOwl"). */
async function pageTitle(page: Page, route: string) {
await page.goto(route);
await expect.poll(() => page.title()).not.toBe("DebateLab");
await expect.poll(() => page.title()).not.toBe("DebateOwl");
return page.title();
}

Expand All @@ -49,13 +49,13 @@ test("loc-titles: every route's tab title is its own and follows the interface l
await evidence.json("01-document-titles", titles);

for (const route of APP_ROUTES) {
for (const language of ["en", "hu", "es"]) expect(titles[language][route]).toMatch(/ · DebateLab$/);
for (const language of ["en", "hu", "es"]) expect(titles[language][route]).toMatch(/ · DebateOwl$/);
}
for (const route of PUBLIC_ROUTES) {
for (const language of ["en", "hu", "es"]) expect(titles[language][route]).toMatch(new RegExp(`\\| ${BRAND}$`));
}
expect(titles.en["/app/generate"]).toBe(`${en.nav.generate} · DebateLab`);
expect(titles.hu["/app/generate"]).toBe(`${hu.nav.generate} · DebateLab`);
expect(titles.en["/app/generate"]).toBe(`${en.nav.generate} · DebateOwl`);
expect(titles.hu["/app/generate"]).toBe(`${hu.nav.generate} · DebateOwl`);
expect(titles.hu["/no-such-page"]).toBe(hu.pageHead.notFound.title.replace("{{brand}}", BRAND));
// Each language names every page in its own words (the brand aside).
for (const route of [...APP_ROUTES, ...PUBLIC_ROUTES]) {
Expand All @@ -65,9 +65,9 @@ test("loc-titles: every route's tab title is its own and follows the interface l
// A switch retitles the page on screen, with no reload.
await page.goto("/app/attack");
await chooseLanguage(page, "en");
await expect(page).toHaveTitle(`${en.nav.attack} · DebateLab`);
await expect(page).toHaveTitle(`${en.nav.attack} · DebateOwl`);
await chooseLanguage(page, "hu");
await expect(page).toHaveTitle(`${hu.nav.attack} · DebateLab`);
await expect(page).toHaveTitle(`${hu.nav.attack} · DebateOwl`);
await evidence.shot("02-attack-title-hu");
});

Expand Down Expand Up @@ -96,7 +96,7 @@ test("loc-switcher: sign-in and not-found pages carry the language switcher", as
await evidence.shot("01-login-en");
await chooseLanguage(page, "hu");
await expect(page.getByRole("heading", { level: 1, name: hu.login.title })).toBeVisible();
// "Bejelentkezés a DebateLabba" already names the app, so it gets no suffix.
// "Bejelentkezés a DebateOwl-ba" already names the app, so it gets no suffix.
await expect(page).toHaveTitle(hu.login.title);
await evidence.shot("02-login-hu");

Expand Down
2 changes: 1 addition & 1 deletion agentic/pages.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const PUBLIC_PAGES = ["/", "/login", "/terms", "/privacy", "/expocit/sources"];
const NAMES: Record<Language, string> = { en: "English", es: "Spanish", hu: "Hungarian" };

const EXEMPT =
"Product names such as DebateLab and ExpoCit, AI model names (Claude Sonnet 5), plan names (Pro), debate format codes and the competition format names kept in English in every language (Public Forum, Lincoln-Douglas, Policy, British Parliamentary, World Schools, Asian Parliamentary), language names offered as choices (English, Español, Magyar), and user content do not count. Technical loanwords the app uses on purpose, such as email, chat, prompt and token, count as translated.";
"Product names such as DebateOwl, DebateLab and ExpoCit, AI model names (Claude Sonnet 5), plan names (Pro), debate format codes and the competition format names kept in English in every language (Public Forum, Lincoln-Douglas, Policy, British Parliamentary, World Schools, Asian Parliamentary), language names offered as choices (English, Español, Magyar), and user content do not count. Technical loanwords the app uses on purpose, such as email, chat, prompt and token, count as translated.";

async function checkPage(browser: Browser, agent: Agent, language: Language) {
await expect(browser.locator("html")).toHaveAttribute("lang", language);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"goneAnchors": [
{
"role": "alertdialog",
"name": "Welcome to DebateLab! 🎉Everything in DebateLab belongs to one debate: the motion you are preparing, its sources, and your results. Start your first debate here.Start debate"
"name": "Welcome to DebateOwl! 🎉Everything in DebateOwl belongs to one debate: the motion you are preparing, its sources, and your results. Start your first debate here.Start debate"
},
{
"role": "button",
Expand All @@ -113,10 +113,10 @@
},
{
"role": "heading",
"name": "Welcome to DebateLab! 🎉"
"name": "Welcome to DebateOwl! 🎉"
},
{
"text": "Everything in DebateLab belongs to one debate: the motion you are preparing, its sources, and your results. Start your first debate here."
"text": "Everything in DebateOwl belongs to one debate: the motion you are preparing, its sources, and your results. Start your first debate here."
},
{
"role": "button",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"endAnchors": [
{
"role": "image",
"name": "Logotipo de DebateLab"
"name": "Logotipo de DebateOwl"
},
{
"text": "Herramientas"
Expand Down Expand Up @@ -69,7 +69,7 @@
"goneAnchors": [
{
"role": "image",
"name": "DebateLab logo"
"name": "DebateOwl logo"
},
{
"text": "Tools"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"goneAnchors": [
{
"role": "alertdialog",
"name": "Welcome to DebateLab! 🎉Everything in DebateLab belongs to one debate: the motion you are preparing, its sources, and your results. Start your first debate here.Start debate"
"name": "Welcome to DebateOwl! 🎉Everything in DebateOwl belongs to one debate: the motion you are preparing, its sources, and your results. Start your first debate here.Start debate"
},
{
"role": "button",
Expand All @@ -113,10 +113,10 @@
},
{
"role": "heading",
"name": "Welcome to DebateLab! 🎉"
"name": "Welcome to DebateOwl! 🎉"
},
{
"text": "Everything in DebateLab belongs to one debate: the motion you are preparing, its sources, and your results. Start your first debate here."
"text": "Everything in DebateOwl belongs to one debate: the motion you are preparing, its sources, and your results. Start your first debate here."
},
{
"role": "button",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"endAnchors": [
{
"role": "image",
"name": "Logotipo de DebateLab"
"name": "Logotipo de DebateOwl"
},
{
"role": "button",
Expand Down Expand Up @@ -70,7 +70,7 @@
"goneAnchors": [
{
"role": "image",
"name": "DebateLab logo"
"name": "DebateOwl logo"
},
{
"role": "button",
Expand Down
4 changes: 2 additions & 2 deletions e2e/first-run.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const USER_ID = '00000000-0000-0000-0000-000000000001';

const copy = {
en: {
welcome: 'Welcome to DebateLab',
welcome: 'Welcome to DebateOwl',
firstDebate: 'Start your first debate here.',
startDebate: 'Start debate',
motionField: 'Motion',
Expand All @@ -24,7 +24,7 @@ const copy = {
gateMotion: 'This House would abolish zoos',
},
hu: {
welcome: 'Üdvözlünk a DebateLabban',
welcome: 'Üdvözlünk a DebateOwl-ban',
firstDebate: 'Itt indíthatod el az első vitádat.',
startDebate: 'Vita indítása',
motionField: 'Tétel',
Expand Down
4 changes: 2 additions & 2 deletions e2e/free-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ test.describe('free debate tools', { tag: '@smoke' }, () => {
expect(response?.ok()).toBeTruthy();
await expect(page.getByRole('heading', { level: 1, name: tool.h1 })).toBeVisible();
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', `${ORIGIN}${tool.path}`);
await expect(page).toHaveTitle(/DebateLab/);
await expect(page).toHaveTitle(/DebateOwl/);
const graphs = await page.locator('script[type="application/ld+json"]').allTextContents();
const types = graphs.flatMap((json) => (JSON.parse(json)['@graph'] as { '@type': string }[]).map((node) => node['@type']));
expect(types).toContain(tool.type);
Expand Down Expand Up @@ -122,7 +122,7 @@ test.describe('free debate tools', { tag: '@smoke' }, () => {
await expect(page.getByRole('heading', { level: 2, name: 'First Proposition' })).toBeVisible();
await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', 'noindex');
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', `${ORIGIN}/tools/debate-timer`);
const attribution = page.getByRole('link', { name: 'Timer by DebateLab' });
const attribution = page.getByRole('link', { name: 'Timer by DebateOwl' });
await expect(attribution).toHaveAttribute('href', `${ORIGIN}/tools/debate-timer`);
await expect(attribution).not.toHaveAttribute('rel', /nofollow/);
await expect(page.getByLabel('Format')).toHaveCount(0);
Expand Down
4 changes: 2 additions & 2 deletions e2e/full-stack/fake-openrouter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const port = Number(process.env.PORT ?? 8787);
const upstream = process.env.UPSTREAM;
const liveModel = process.env.LIVE_MODEL;
const chatModel = (requested) => (liveModel === "as-requested" ? requested : liveModel);
// Claude runs only on DebateLab's Anthropic key (BYOK in OpenRouter): every
// Claude runs only on DebateOwl's Anthropic key (BYOK in OpenRouter): every
// Claude call leaves pinned to the Anthropic provider with fallbacks off, and
// any `models` fallback list is dropped. A rewrite to another family (only
// as-requested reaches one) keeps just the options that still apply to it.
Expand Down Expand Up @@ -191,7 +191,7 @@ const routes = [
["brainstorm", "Brainstorm arguments like experienced competitive debaters", brainstorm],
["factoids", "You are a debate research specialist", factoids],
["sparring", "live debate sparring opponent", (user, system) => sparring(user, system)],
["crossfire", "You are DebateLab's crossfire coach", (user) => crossfire(user)],
["crossfire", "You are DebateOwl's crossfire coach", (user) => crossfire(user)],
["ask", "You are a debate coach chat bot", ask],
];

Expand Down
4 changes: 2 additions & 2 deletions e2e/full-stack/first-run.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const copy = {
demoTab: "Demo code",
demoCode: "Demo code",
signIn: "Sign in with demo code",
welcome: "Welcome to DebateLab",
welcome: "Welcome to DebateOwl",
firstDebate: "Start your first debate here.",
startDebate: "Start debate",
motionField: "Motion",
Expand All @@ -29,7 +29,7 @@ const copy = {
demoTab: "Demókód",
demoCode: "Demókód",
signIn: "Bejelentkezés demókóddal",
welcome: "Üdvözlünk a DebateLabban",
welcome: "Üdvözlünk a DebateOwl-ban",
firstDebate: "Itt indíthatod el az első vitádat.",
startDebate: "Vita indítása",
motionField: "Tétel",
Expand Down
2 changes: 1 addition & 1 deletion e2e/home-navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ test('header keeps the brand and controls inside a 320px viewport', async ({ pag
const debateControl = page.getByTestId('current-debate-mobile');
const controls = [languageControl, menuControl, debateControl];

await expect(page.getByAltText('DebateLab logo')).toBeVisible();
await expect(page.getByAltText('DebateOwl logo')).toBeVisible();
for (const control of controls) await expect(control).toBeVisible();

const boxes = await Promise.all(controls.map((control) => control.boundingBox()));
Expand Down
Loading
Loading