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
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ Preconditions:
- `Save` appears both on the result toolbar and in the dialog. Scope the second click to `getByRole("dialog")`. The toolbar `Save` exists only for one-off results.
- A case filed in a debate is titled with the motion, which the header control also shows (truncated and hidden). Look for it inside the History panel, not with a page-wide `getByText`.
- The client files a result after it renders. Wait for the result on screen (and the row, if the next step reloads) before a `page.goto`, or the reload can drop it.
- `recipes/case-examples.spec.ts` is not a check: it generates the public `/debate-case-example` samples (a debate with a format setting, then R1 and R2) and runs only on a live stack with `CASE_EXAMPLES=all` or a sample id. `scripts/case-examples/README.md` covers the run.
- The model label in the result header (for example `anthropic/claude-sonnet-5.5`) is the model the function requested. The answer itself came from the fake provider.
- `Google Docs` export needs Google OAuth and cannot be proven locally.
71 changes: 71 additions & 0 deletions .claude/skills/verify-debatelab/recipes/case-examples.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { admin, aiResponse, debateControl, expect, LIVE, providerCalls, test } from "./fixtures";

// Generates the samples on the public /debate-case-example page through the real
// app: Login, Start debate with a format, Generate as R1, then R2 in the same debate.
// It spends live model and Exa calls, so it runs only when asked:
// VERIFY_LIVE_MODEL=anthropic/claude-sonnet-5.5 $H up --live
// CASE_EXAMPLES=all $H drive recipes/case-examples --project desktop-chromium (or CASE_EXAMPLES=bp-en)
// scripts/case-examples/README.md says how the evidence becomes the committed raw files.
const SAMPLES = [
{ id: "bp-en", motion: "This House would lower the voting age to 16", format: "British Parliamentary", side: "In favor", language: undefined },
{ id: "policy-en", motion: "Resolved: The United States federal government should substantially increase its investment in high-speed rail", format: "Policy", side: "Against", language: undefined },
{ id: "classroom-hu", motion: "Ez a Ház betiltaná a házi feladatot az általános iskolákban", format: "Classroom", side: "In favor", language: "Magyar" },
] as const;

const selected = process.env.CASE_EXAMPLES;

for (const sample of SAMPLES) {
test(`case example ${sample.id}`, async ({ signedIn: page, user, evidence }) => {
test.skip(!LIVE || !selected || (selected !== "all" && selected !== sample.id), "set CASE_EXAMPLES on a live stack to generate the public samples");
test.setTimeout(30 * 60_000);

if (sample.language) {
await page.getByRole("button", { name: "Language", exact: true }).first().click();
await page.getByRole("radiogroup", { name: "AI output language" }).getByRole("radio", { name: sample.language }).click();
await page.keyboard.press("Escape");
await evidence.step(`AI output language: ${sample.language}`);
}

if (!page.url().includes("/app")) await page.goto("/app");
await debateControl(page).click();
const dialog = page.getByRole("dialog");
await dialog.getByLabel("Motion").fill(sample.motion);
await dialog.getByRole("combobox", { name: "Format" }).click();
await page.getByRole("option", { name: sample.format, exact: true }).click();
await evidence.shot("00-start-debate");
await dialog.getByRole("button", { name: "Start debate" }).click();
await expect(dialog).toBeHidden();
await evidence.step(`Started debate "${sample.motion}" with format ${sample.format}`);

await page.goto("/app/generate");
await expect(page.getByRole("heading", { name: "Your case will appear here" })).toBeVisible();
await expect(page.getByRole("textbox", { name: "Motion / topic *" })).toHaveValue(sample.motion);
await page.getByRole("combobox", { name: "Side" }).click();
await page.getByRole("option", { name: sample.side, exact: true }).click();

for (const role of ["R1 - Rebuttal/Arg (6 min)", "R2 - Rebuttal/Build (6 min)"]) {
const key = role.startsWith("R1") ? "r1" : "r2";
await page.getByRole("combobox", { name: "Speaker Role" }).click();
await page.getByRole("option", { name: role }).click();
const before = (await providerCalls()).length;
const response = aiResponse(page, "generate");
const requestPromise = page.waitForRequest((request) => request.url().endsWith("/functions/v1/ai") && request.method() === "POST");
await page.getByRole("button", { name: "Generate debate case" }).click();
const request = await requestPromise;
const body = await response;
await evidence.json(`${key}-request`, request.postDataJSON());
await evidence.json(`${key}-response`, body);
await evidence.json(`${key}-provider-calls`, (await providerCalls()).slice(before));
const thesis = (body.output as { caseStructure: { thesis: string } }).caseStructure.thesis;
await expect(page.getByText(thesis).first()).toBeVisible();
await evidence.shot(`${key}-generated`);
await evidence.step(`Generated ${key}`);
}

const userId = await user.userId();
const { data: requests } = await admin.from("ai_requests").select("*").eq("user_id", userId);
await evidence.json("ai-requests", requests);
const { data: cases } = await admin.from("cases").select("id,title,side,created_at,debates(motion,settings)").eq("user_id", userId);
await evidence.json("case-rows", cases);
});
}
2 changes: 1 addition & 1 deletion docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The landing page and the free tools also exist in Hungarian and Spanish, each at

[`scripts/sitemap.mjs`](../scripts/sitemap.mjs) then writes `dist/sitemap.xml` from every page in `dist/` that names itself as canonical and is not `noindex`. A page with `hreflang` links in its head is listed with the same alternates as `xhtml:link` entries. Each `<lastmod>` is the `dateModified` the page declares in its JSON-LD: the guide pages' `CONTENT_DATE` in [`scripts/generate_seo_content.py`](../scripts/generate_seo_content.py), the legal pages' "last updated" dates, the tool pages' `TOOL_PAGES_MODIFIED`, and the landing page's last source commit. A page without a date is listed without `<lastmod>`.

[`scripts/llms-full.mjs`](../scripts/llms-full.mjs) then writes `dist/llms-full.txt`: `llms.txt` followed by the visible text of `/`, `/about`, `/best-ai-for-debate` and `/press`, read from their built HTML. It is a build output, not a file in `public/`.
[`scripts/llms-full.mjs`](../scripts/llms-full.mjs) then writes `dist/llms-full.txt`: `llms.txt` followed by the visible text of `/`, `/about`, `/best-ai-for-debate`, `/press` and `/debate-case-example`, read from their built HTML. It is a build output, not a file in `public/`.

[`scripts/check-site.mjs`](../scripts/check-site.mjs) runs next and fails the build when an internal link, a `#fragment`, a sitemap URL or an `llms.txt` URL does not resolve (it applies the `vercel.json` redirects and rewrites), when JSON-LD does not parse or carries HTML entities, when a FAQPage question is not visible on its page, when the home page's entity graph has an unresolved `@id`, a `WebSite` or `SoftwareApplication` not named for the product, or no `Organization.sameAs`, when a page's `publisher` or `about` names an `@id` the home page does not define, when an indexed page other than `/terms`, `/privacy`, and `/expocit/sources` lacks its question H2 (`#short-answer`) as the first H2 (a page whose H1 is already that question may instead put the answer in a `#short-answer` paragraph before the first H2), when a page's first heading is not its H1 or a heading skips a level (H1 to H3), when an image has no alt text, a button has no name or a form field has no label, when two indexed pages share a `<title>`, or when a page's `hreflang` links do not resolve, leave out the page itself or `x-default`, disagree with `<html lang>`, or differ from the set on a page they name.

Expand Down
Loading
Loading