Skip to content

Commit 68c53f5

Browse files
authored
Merge pull request #61 from Programcode/t3code/ponytail-audit
refactor: ponytail audit cuts 3,633 net lines and 7 dependencies
2 parents 7ed22d0 + af9347e commit 68c53f5

91 files changed

Lines changed: 2689 additions & 6304 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/performance/smooth-navigation-baseline/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ requests, so they prove rendering and browser timing only.
1212

1313
## Reproduce
1414

15+
The harness `scripts/measure-navigation-performance.mjs` was a single-use
16+
baseline tool and was removed once this evidence was captured. Recover it with
17+
`git show 9127589:scripts/measure-navigation-performance.mjs` if the matrix ever
18+
needs re-running. The commands below are the ones that produced `run-1.json`
19+
and `run-2.json`.
20+
1521
```text
1622
export PATH=/home/codex/.nvm/versions/node/v22.23.2/bin:$PATH
1723
node --version # v22.23.2

‎e2e/animation-polish.spec.ts‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { expect, test, type Page } from '@playwright/test';
22
import { stubSupabaseSession } from './helpers/auth';
3+
import { stubAIResponses } from './fixtures/ai-responses';
34

4-
const featureConfig = { provider: 'mock', model: 'local', temperature: 0.7 };
5+
const featureConfig = { provider: 'openrouter', model: 'z-ai/glm-5.3-flash', temperature: 0.7 };
56
const STATE_TIMEOUT_MS = 15_000;
67

78
async function prepareMotionFixture(page: Page, reducedMotion: 'no-preference' | 'reduce') {
89
await page.emulateMedia({ reducedMotion });
910
await stubSupabaseSession(page);
11+
await stubAIResponses(page);
1012
await page.addInitScript((config) => {
1113
window.localStorage.setItem('debate-trainer-settings', JSON.stringify({
1214
state: {
@@ -47,10 +49,27 @@ async function openGenerateOptions(page: Page) {
4749
async function generateMockCase(page: Page) {
4850
await page.getByLabel('Motion / Topic').fill('This House would make public transport free.');
4951
await page.getByRole('button', { name: 'Generate Debate Case' }).click();
52+
await page.mouse.move(0, 0);
5053
await expect(page.getByText('Reading the motion...')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
5154
await expect(page.getByText(/We support the motion/)).toBeVisible({ timeout: STATE_TIMEOUT_MS });
5255
}
5356

57+
async function openFeatureRoute(page: Page, route: 'brainstorm' | 'factoids' | 'ask') {
58+
const desktopLink = page.locator(`[data-tour="nav-${route}"]`).first();
59+
60+
if ((page.viewportSize()?.width ?? 1280) < 1024) {
61+
await page.getByRole('button', { name: 'Open menu' }).click();
62+
await page.getByRole('navigation', { name: 'Mobile navigation' })
63+
.locator(`a[href="/app/${route}"]`)
64+
.click();
65+
} else {
66+
if (!await desktopLink.isVisible()) await page.locator('summary[data-tour="nav-more"]').click();
67+
await desktopLink.click();
68+
}
69+
70+
await expect(page).toHaveURL(new RegExp(`/app/${route}$`));
71+
}
72+
5473
test('keeps route and feature-state motion short and stable', async ({ page }, testInfo) => {
5574
await prepareMotionFixture(page, 'no-preference');
5675
const options = await openGenerateOptions(page);
@@ -139,21 +158,21 @@ test('audits applicable empty, loading, result, and option states across feature
139158
await expect(page.getByRole('heading', { name: 'Overall Assessment' })).toBeVisible({ timeout: STATE_TIMEOUT_MS });
140159
await page.screenshot({ path: testInfo.outputPath('attack-result.png'), fullPage: true });
141160

142-
await page.goto('/app/brainstorm', { waitUntil: 'domcontentloaded' });
161+
await openFeatureRoute(page, 'brainstorm');
143162
await expect(page.getByText('Unlock Creative Angles')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
144163
await page.getByLabel('Motion / Topic').fill('This House would make public transport free.');
145164
await page.getByRole('button', { name: 'Start Brainstorming' }).click();
146165
await expect(page.getByText('Exploring angles...')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
147166
await expect(page.getByRole('heading', { name: 'Top 5 Argument Angles' })).toBeVisible({ timeout: STATE_TIMEOUT_MS });
148167

149-
await page.goto('/app/factoids', { waitUntil: 'domcontentloaded' });
168+
await openFeatureRoute(page, 'factoids');
150169
await expect(page.getByText('Evidence That Wins')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
151170
await page.getByLabel('Statement').fill('Free public transport reduces urban congestion.');
152171
await page.getByRole('button', { name: 'Find Factoids' }).click();
153172
await expect(page.getByText('Searching databases...')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
154173
await expect(page.getByRole('heading', { name: 'Factoids' })).toBeVisible({ timeout: STATE_TIMEOUT_MS });
155174

156-
await page.goto('/app/ask', { waitUntil: 'domcontentloaded' });
175+
await openFeatureRoute(page, 'ask');
157176
await expect(page.getByText('Start a conversation')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
158177
await page.getByPlaceholder('Ask your debate coach...').fill('How should I weigh access against cost?');
159178
await page.getByRole('button', { name: 'Send message' }).click();

‎e2e/fixtures/ai-responses.ts‎

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* Canned edge-function responses for specs that exercise UI states rather than
3+
* AI quality. Intercepting the network keeps the app's real request path — and
4+
* its schema validation — under test, unlike the deleted in-app mock provider
5+
* which bypassed both.
6+
*/
7+
import type { Page } from '@playwright/test';
8+
9+
const CORS_HEADERS = {
10+
'access-control-allow-origin': '*',
11+
'access-control-allow-headers': 'authorization, x-client-info, apikey, content-type',
12+
'access-control-allow-methods': 'POST, OPTIONS',
13+
'content-type': 'application/json',
14+
};
15+
16+
// The client rejects a generate response whose contention count does not match
17+
// the requested length (see expectedContentionCount in src/lib/ai/client.ts).
18+
const CONTENTION_COUNT: Record<string, number> = { brief: 2, standard: 3, comprehensive: 4 };
19+
20+
const CONTENTIONS = [
21+
{
22+
id: '1',
23+
claim: 'Access improves for the people who need it most',
24+
reasoning: 'Fares are a regressive barrier concentrated on low-income riders.',
25+
evidence: 'Comparable jurisdictions report ridership gains after fare removal.',
26+
needsSource: false,
27+
impact: 'More people reach work, school, and healthcare.',
28+
},
29+
{
30+
id: '2',
31+
claim: 'Congestion and emissions fall as mode share shifts',
32+
reasoning: 'Removing the marginal cost of a trip shifts journeys off the road.',
33+
evidence: 'Mode-share studies show measurable shifts at zero fare.',
34+
needsSource: false,
35+
impact: 'Shorter journeys and lower urban emissions.',
36+
},
37+
{
38+
id: '3',
39+
claim: 'Fare collection overhead outweighs much of the revenue it protects',
40+
reasoning: 'Gates, inspectors, and ticketing systems consume a large share of receipts.',
41+
evidence: 'Operators report collection costs well above headline estimates.',
42+
needsSource: false,
43+
impact: 'The net funding gap is far smaller than opponents claim.',
44+
},
45+
{
46+
id: '4',
47+
claim: 'Civic participation rises when mobility is not rationed by price',
48+
reasoning: 'Unpriced trips are taken for non-essential civic and social reasons too.',
49+
evidence: 'Ridership growth after fare removal skews to off-peak trips.',
50+
needsSource: false,
51+
impact: 'Denser civic life, not just cheaper commutes.',
52+
},
53+
];
54+
55+
const generate = (motion: string, length: string) => ({
56+
caseStructure: {
57+
thesis: `We support the motion that ${motion}`,
58+
framework: 'Net societal benefit, weighed on access and cost.',
59+
keyDefinitions: ['Public transport: publicly funded scheduled transit.'],
60+
burdenOfProof: 'We must show the policy produces a net gain in practice.',
61+
},
62+
contentions: CONTENTIONS.slice(0, CONTENTION_COUNT[length] ?? 2),
63+
predictedOpponentCase: {
64+
likelyArguments: ['The lost farebox revenue has to come from somewhere.'],
65+
strategicWeaknesses: ['Assumes fare revenue is the only funding model.'],
66+
keyClashes: ['Who actually bears the cost.'],
67+
},
68+
counters: [
69+
{
70+
targetArgument: 'Funding gap',
71+
counterpoint: 'Fare collection itself carries substantial overhead.',
72+
rebuttal: 'Net cost is far below headline farebox revenue.',
73+
},
74+
],
75+
metadata: { side: 'pro', format: 'bp', generatedAt: new Date().toISOString() },
76+
});
77+
78+
const attack = {
79+
overallAssessment: {
80+
score: 62,
81+
summary: 'The case is coherent but leaves its central warrant underdefended.',
82+
strengthAreas: ['Clear thesis', 'Explicit framework'],
83+
},
84+
weaknesses: [
85+
{
86+
id: 'w1',
87+
severity: 'major',
88+
type: 'evidential',
89+
description: 'The congestion claim has no comparative evidence.',
90+
whyWeak: 'Correlation between fare policy and congestion is asserted, not shown.',
91+
howOpponentAttacks: 'Name a city where ridership rose and congestion did not fall.',
92+
improvedVersion: 'Cite a before/after study with a control corridor.',
93+
},
94+
],
95+
fallaciesDetected: [
96+
{ name: 'Hasty generalization', location: 'Second contention', explanation: 'One city generalized to all.' },
97+
],
98+
crossExQuestions: [
99+
{ question: 'Which funding line replaces the farebox?', purpose: 'Force a concrete mechanism.', expectedTrap: 'A vague appeal to general taxation.' },
100+
],
101+
weighingAnalysis: {
102+
currentWeighing: 'Access is asserted to outweigh cost without comparison.',
103+
missingComparisons: ['Cost per additional rider versus targeted subsidy.'],
104+
improvementSuggestions: ['Weigh against the strongest alternative, not the status quo.'],
105+
},
106+
};
107+
108+
const idea = {
109+
id: 'i1',
110+
title: 'Access as a precondition for participation',
111+
description: 'Mobility gates work, education, and civic life.',
112+
side: 'pro',
113+
strength: 'strong',
114+
tradeoffs: ['Requires a durable funding source.'],
115+
relatedTo: [],
116+
};
117+
118+
const brainstorm = {
119+
needsMoreInfo: false,
120+
topFive: [{ rank: 1, idea, whyTop: 'It reaches the widest impact with the clearest warrant.' }],
121+
nextSteps: ['Find a before/after ridership study.'],
122+
warningsAndTradeoffs: ['Do not overclaim the congestion effect.'],
123+
};
124+
125+
const factoids = {
126+
factoids: [
127+
{
128+
id: 'f1',
129+
shortClaim: 'Fare removal raised ridership in Tallinn',
130+
keyFigure: '+14% ridership',
131+
context: 'Measured over the first year of the free-fare scheme.',
132+
limitations: 'A single city with unusual baseline car ownership.',
133+
citation: { title: 'Free public transport evaluation', publisher: 'Municipal transport authority', verified: 'verified' },
134+
howToUse: 'Use as an existence proof, not as a universal rate.',
135+
relevanceScore: 80,
136+
},
137+
],
138+
searchSuggestions: ['fare-free transit ridership study'],
139+
warnings: [],
140+
verificationStatus: 'complete',
141+
};
142+
143+
const ask = (message: string) => ({
144+
response: `Great question about ${message.slice(0, 60)} — weigh access against cost by naming who bears each.`,
145+
followUpQuestions: ['What is the strongest alternative policy?'],
146+
uncertainties: ['The congestion effect size varies widely by city.'],
147+
});
148+
149+
type AiRequest = { feature?: string; input?: Record<string, unknown> };
150+
151+
// The deleted mock provider delayed 1-2s per feature, which is what made the
152+
// loading states observable. Specs assert on those states, so keep the latency.
153+
const RESPONSE_DELAY_MS = 1_200;
154+
155+
/**
156+
* Serves canned output for every `ai` edge-function call. Specs that need a
157+
* specific payload should add their own narrower `page.route` first.
158+
*/
159+
export async function stubAIResponses(page: Page) {
160+
await page.route('**/functions/v1/ai**', async (route) => {
161+
if (route.request().method() === 'OPTIONS') {
162+
await route.fulfill({ status: 200, headers: CORS_HEADERS, body: 'ok' });
163+
return;
164+
}
165+
const body = (route.request().postDataJSON() ?? {}) as AiRequest;
166+
const motion = typeof body.input?.motion === 'string' ? body.input.motion : 'the motion';
167+
const message = typeof body.input?.message === 'string' ? body.input.message : 'this';
168+
const length = typeof body.input?.length === 'string' ? body.input.length : 'brief';
169+
const output = {
170+
generate: () => generate(motion, length),
171+
attack: () => attack,
172+
brainstorm: () => brainstorm,
173+
factoids: () => factoids,
174+
ask: () => ask(message),
175+
}[body.feature ?? '']?.();
176+
177+
if (!output) {
178+
await route.fulfill({ status: 400, headers: CORS_HEADERS, body: JSON.stringify({ error: `No fixture for feature ${body.feature}` }) });
179+
return;
180+
}
181+
await new Promise((resolve) => setTimeout(resolve, RESPONSE_DELAY_MS));
182+
await route.fulfill({ status: 200, headers: CORS_HEADERS, body: JSON.stringify({ output, discoveredSources: [] }) });
183+
});
184+
}

‎e2e/home-navigation.spec.ts‎

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,27 +60,19 @@ test('onboarding follows visible home targets when secondary navigation is close
6060

6161
test('onboarding keeps its original start schedule after a delayed lazy chunk', async ({ page }) => {
6262
await page.addInitScript(() => {
63-
const originalSetItem = Storage.prototype.setItem;
64-
Storage.prototype.setItem = function setItem(key, value) {
65-
if (key === 'debate-trainer-onboarding') {
66-
try {
67-
if (JSON.parse(value).state?.isTourRunning === true) {
68-
originalSetItem.call(this, '__onboarding-tour-started-at', String(Date.now()));
69-
}
70-
} catch {
71-
// Ignore malformed test-only storage writes.
72-
}
73-
}
74-
originalSetItem.call(this, key, value);
75-
};
7663
window.localStorage.setItem('debate-trainer-onboarding', JSON.stringify({
7764
state: { hasCompletedOnboarding: false, isTourRunning: false },
7865
version: 0,
7966
}));
8067
});
8168
await page.route('**/src/components/features/onboarding/OnboardingTour.tsx*', async (route) => {
8269
const response = await route.fetch();
83-
const body = `${await response.text()}\nwindow.localStorage.setItem('__onboarding-module-evaluated-at', String(Date.now()));`;
70+
const source = await response.text();
71+
const body = source.replace(
72+
/const\s+timer\s*=\s*setTimeout\(\(\)\s*=>\s*\{/,
73+
'window.localStorage.setItem("__onboarding-auto-start-delay", String(autoStartDelay));\n const timer = setTimeout(() => {',
74+
);
75+
if (body === source) throw new Error('Unable to instrument the onboarding auto-start delay');
8476
await new Promise((resolve) => setTimeout(resolve, 750));
8577
await route.fulfill({ response, body });
8678
});
@@ -92,11 +84,10 @@ test('onboarding keeps its original start schedule after a delayed lazy chunk',
9284

9385
await page.goto('/app', { waitUntil: 'domcontentloaded' });
9486

95-
await expect.poll(async () => Number(await page.evaluate(() => window.localStorage.getItem('__onboarding-module-evaluated-at') ?? 0)), { timeout: 15_000 }).toBeGreaterThan(0);
96-
await expect.poll(async () => Number(await page.evaluate(() => window.localStorage.getItem('__onboarding-tour-started-at') ?? 0)), { timeout: 15_000 }).toBeGreaterThan(0);
97-
const moduleEvaluatedAt = await page.evaluate(() => Number(window.localStorage.getItem('__onboarding-module-evaluated-at')));
98-
const startedAt = await page.evaluate(() => Number(window.localStorage.getItem('__onboarding-tour-started-at')));
99-
expect(startedAt - moduleEvaluatedAt).toBeLessThan(450);
87+
await expect.poll(
88+
() => page.evaluate(() => Number(window.localStorage.getItem('__onboarding-auto-start-delay') ?? -1)),
89+
{ timeout: 15_000 },
90+
).toBe(0);
10091
await expect(page.locator('.react-joyride__tooltip')).toBeVisible({ timeout: 15_000 });
10192
});
10293

‎e2e/hungarian-parity.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ test.describe('Hungarian parity', () => {
140140
await page.getByRole('button', { name: 'Forrás Hozzáadása' }).click();
141141
await expect(page.getByText('Importálás URL-ből')).toBeVisible();
142142
await page.getByRole('button', { name: 'Mentés a Könyvtárba' }).click();
143-
await expect(page.locator('[toast-close]')).toHaveAttribute('aria-label', 'Bezárás');
143+
await expect(page.locator('[data-sonner-toast] [data-close-button]')).toHaveAttribute('aria-label', 'Bezárás');
144144

145145
await page.goto('/app/flowchart', { waitUntil: 'domcontentloaded' });
146146
await page.getByRole('button', { name: 'Betöltés' }).click();

‎e2e/navigation-performance.spec.ts‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ async function openAppNav(page: Page, label: string) {
5858
}
5959

6060
test('cold routes expose real content on both browser profiles', async ({ page }) => {
61+
test.setTimeout(60_000);
6162
await setLocale(page, 'en');
6263
for (const [path, selector] of publicRoutes) {
6364
await page.goto(path, { waitUntil: 'domcontentloaded' });
@@ -79,6 +80,17 @@ test('cold routes expose real content on both browser profiles', async ({ page }
7980
await appPage.close();
8081
});
8182

83+
test('workspaces stay unavailable without a collaboration endpoint', async ({ page }) => {
84+
await setLocale(page, 'en');
85+
await stubSupabaseSession(page);
86+
await page.goto('/app', { waitUntil: 'domcontentloaded' });
87+
await waitForContent(page, { selector: 'main h1', text: 'DebateLab' });
88+
89+
await expect(page.locator('a[href="/app/workspaces"]')).toHaveCount(0);
90+
await page.goto('/app/workspaces', { waitUntil: 'domcontentloaded' });
91+
await expect(page.getByRole('heading', { name: '404' })).toBeVisible();
92+
});
93+
8294
test('a delayed public route keeps the main landmark', async ({ page }) => {
8395
await setLocale(page, 'en');
8496
let releaseTerms: (() => void) | undefined;
@@ -305,9 +317,13 @@ test('hover intent is limited to fine pointers and click stays in-document', asy
305317
expect(requestCount()).toBe(0);
306318

307319
await link.hover();
308-
await page.waitForTimeout(INTENT_OBSERVATION_MS);
309320
const supportsHover = await page.evaluate(() => window.matchMedia('(hover: hover) and (pointer: fine)').matches);
310-
expect(requestCount()).toBe(supportsHover ? 1 : 0);
321+
if (supportsHover) {
322+
await expect.poll(requestCount).toBe(1);
323+
} else {
324+
await page.waitForTimeout(INTENT_OBSERVATION_MS);
325+
expect(requestCount()).toBe(0);
326+
}
311327

312328
const navigationEntriesBeforeClick = await page.evaluate(() => performance.getEntriesByType('navigation').length);
313329
await startGenericLoaderObservation(page);
@@ -327,8 +343,7 @@ test('keyboard focus fetches one destination chunk before click', async ({ page
327343

328344
await link.focus();
329345
await expect(link).toBeFocused();
330-
await page.waitForTimeout(INTENT_OBSERVATION_MS);
331-
expect(requestCount()).toBe(1);
346+
await expect.poll(requestCount).toBe(1);
332347

333348
await startGenericLoaderObservation(page);
334349
await link.click();
@@ -345,8 +360,7 @@ test('touch intent fetches one destination chunk before click', async ({ page })
345360
expect(requestCount()).toBe(0);
346361

347362
await link.dispatchEvent('pointerdown', { pointerType: 'touch' });
348-
await page.waitForTimeout(INTENT_OBSERVATION_MS);
349-
expect(requestCount()).toBe(1);
363+
await expect.poll(requestCount).toBe(1);
350364

351365
await startGenericLoaderObservation(page);
352366
await link.click();

0 commit comments

Comments
 (0)