From 3faccce2483ee315ad4dcd4be2ff35155641eaa1 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Sep 2026 21:07:06 +0000 Subject: [PATCH 1/2] chore: remove dead code and tautological tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dead code (zero references anywhere, including tests): - src/lib/ai/prompts/index.ts (165 lines) — prompt building moved server-side to supabase/functions/_shared/prompts.ts - useKeyboardShortcuts, useResolvedTheme hooks - isUrlSafe (validateUrl is the live entry point) - requireAdmin in _shared/auth.ts — admin/index.ts does its own inline user_roles lookups - getAppRedirectUrl in native.ts - mockSparringResponse/mockSparringScore aliases - buildSearchQueries wrapper — inlined the one-line .map into its two callers, which were both tests Tests that assert nothing about our code: - example.test.ts: expect(true).toBe(true) - utils.test.ts: 11 tests over cn(), a one-line clsx+tailwind-merge pass-through — tested the libraries, not us - providers.test.ts: 14 of 15 tests re-asserted that a static const is still a static const; kept the duplicate-model-ID check, which guards real routing behavior - i18n.test.ts: "locale has keys" tautologies and a key-count check subsumed by the bidirectional key-set comparisons - export-utils-extended.test.ts: 6 "typeof x === function" tests - pages/HistoryPage smoke tests asserting document.body is defined - settings-store-branches: the `afterRehydrate(undefined)` call existed only to hit an `if (!state) return` line Preserved deliberately: setActiveEnv/IS_STAGING in supabase/env.ts (reserved by plans/supabase-staging.md for the admin env switcher), and security-hardening/runtime-contracts/vercel-deployment-config tests, which guard invariants from PRs #26/#27. Verified: 921 tests pass, lint clean, tsc clean, build succeeds, coverage thresholds still met (statements 90.45/90, branches 82.91/80, functions 91.46/89, lines 94.04/90). Co-Authored-By: Claude Opus 5 --- src/hooks/use-keyboard-shortcuts.ts | 26 --- src/hooks/use-resolved-theme.ts | 35 ---- src/lib/ai/prompts/index.ts | 165 ------------------ src/lib/ai/providers/mock-provider.ts | 2 - src/lib/native.ts | 5 - src/lib/validation/url.ts | 8 - src/test/example.test.ts | 7 - src/test/export-utils-extended.test.ts | 25 --- src/test/i18n.test.ts | 30 +--- src/test/pages/HistoryPage.test.tsx | 5 - src/test/pages/pages.test.tsx | 15 -- src/test/providers.test.ts | 86 +-------- .../stores/settings-store-branches.test.ts | 3 - src/test/utils.test.ts | 90 ---------- src/test/validation.test.ts | 14 +- supabase/functions/_shared/auth.ts | 15 -- supabase/functions/_shared/source-quality.ts | 4 - supabase/functions/ai/source-quality.test.ts | 6 +- 18 files changed, 8 insertions(+), 533 deletions(-) delete mode 100644 src/hooks/use-keyboard-shortcuts.ts delete mode 100644 src/hooks/use-resolved-theme.ts delete mode 100644 src/lib/ai/prompts/index.ts delete mode 100644 src/test/example.test.ts delete mode 100644 src/test/utils.test.ts diff --git a/src/hooks/use-keyboard-shortcuts.ts b/src/hooks/use-keyboard-shortcuts.ts deleted file mode 100644 index 168b643fb..000000000 --- a/src/hooks/use-keyboard-shortcuts.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect } from 'react'; - -export function useKeyboardShortcuts(shortcuts: Record void>) { - useEffect(() => { - const handler = (e: KeyboardEvent) => { - const target = e.target; - if ( - target instanceof HTMLElement && - (target.tagName === 'INPUT' || - target.tagName === 'TEXTAREA' || - target.tagName === 'SELECT' || - target.isContentEditable) - ) { - return; - } - - const key = `${e.metaKey || e.ctrlKey ? 'mod+' : ''}${e.key.toLowerCase()}`; - if (shortcuts[key]) { - e.preventDefault(); - shortcuts[key](); - } - }; - window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); - }, [shortcuts]); -} diff --git a/src/hooks/use-resolved-theme.ts b/src/hooks/use-resolved-theme.ts deleted file mode 100644 index 2ee5ab841..000000000 --- a/src/hooks/use-resolved-theme.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useSyncExternalStore } from 'react'; -import { useSettingsStore } from '@/lib/stores/settings-store'; - -const SYSTEM_THEME_QUERY = '(prefers-color-scheme: dark)'; - -function subscribe(callback: () => void) { - if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { - return () => {}; - } - const mql = window.matchMedia(SYSTEM_THEME_QUERY); - mql.addEventListener('change', callback); - return () => mql.removeEventListener('change', callback); -} - -function getSnapshot(): 'light' | 'dark' { - if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { - return 'light'; - } - return window.matchMedia(SYSTEM_THEME_QUERY).matches ? 'dark' : 'light'; -} - -function getServerSnapshot(): 'light' | 'dark' { - return 'light'; -} - -export function useResolvedTheme(): 'light' | 'dark' { - const theme = useSettingsStore((state) => state.theme); - const systemTheme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); - - if (theme === 'system') { - return systemTheme; - } - - return theme; -} diff --git a/src/lib/ai/prompts/index.ts b/src/lib/ai/prompts/index.ts deleted file mode 100644 index ceb307061..000000000 --- a/src/lib/ai/prompts/index.ts +++ /dev/null @@ -1,165 +0,0 @@ -// src/lib/ai/prompts/index.ts - -export const SYSTEM_PROMPTS = { - generate: `You are an elite debate coach and world-class argument architect. Your goal is to craft winning cases that sound like they were written by a human champion: persuasive, logical, and confident. - -CRITICAL GUIDELINES: -1. **Tone:** Be assertive and natural. Use rhetorical flair but keep it grounded. Avoid robotic transitions like "It is important to note" or "This demonstrates that." Instead, use active voice: "This proves," "Crucially," or simply state the point. -2. **Structure:** Every argument needs a clear Claim, Warrant (reasoning), and Impact. -3. **Evidence:** If you need a stat/source you don't have, write [NEEDS SOURCE: specific search query]. NEVER make up citations. -4. **Strategy:** Pre-emptively crush common counter-arguments. Think big picture: why does this matter in the real world? -5. **Format:** You MUST output valid JSON matching the schema. - -Your output should be ready to read in a tournament round. Make it flow. Make it sting. Make it win.`, - - attack: `You are a ruthless, sharp-witted debate judge and opponent. Your job is to tear arguments apart, not just to find nitpicks, but to destroy the core logic. - -CRITICAL GUIDELINES: -1. **Tone:** Direct, skeptical, and incisive. Don't be polite; be effective. Call out weak links, logical leaps, and missing impacts. -2. **Severity:** Distinguish between a "glitch" (minor) and a "game over" (critical) flaw. -3. **Actionable:** Don't just say it's bad; tell the user exactly how to fix it or how an opponent would exploit it. -4. **Paradigm:** Adapt to the requested judge type (Logic, Values, or Policy). -5. **Format:** You MUST output valid JSON matching the schema. - -Find the gap in their armor and drive a truck through it.`, - - brainstorm: `You are a creative, out-of-the-box debate strategist. We are here to invent, explore, and break the mold. - -CRITICAL GUIDELINES: -1. **Tone:** Collaborative, energetic, and open-minded. Use casual, conversational transitions. "What if we looked at it this way?" or "Here's a wild angle..." -2. **Quality:** Go beyond the obvious. If the motion is about economy, look for the social or philosophical angle. -3. **Breadth:** Offer distinct paths, don't just give 5 variations of the same point. -4. **Honesty:** If an idea is high-risk, say so. "This is risky, but high reward." -5. **Format:** You MUST output valid JSON matching the schema. - -Help the user see what everyone else is missing.`, - - factoids: `You are a deeply skeptical, evidence-obsessed research assistant. You demand proof for everything. - -CRITICAL GUIDELINES: -1. **Truth:** NEVER fabricate a citation. If you aren't 100% sure it exists, mark it "UNVERIFIED" and say why. -2. **Context:** A number without context is useless. Explain *why* this stat matters in a debate round. -3. **Sourcing:** Prioritize primary sources (studies, government data) over opinion pieces. -4. **Tone:** Professional, precise, and efficient. -5. **Format:** You MUST output valid JSON matching the schema. - -Your goal is to arm the user with bulletproof facts.`, - - ask: `You are a wise, experienced debate coach who has seen it all. You are here to mentor, not just answer. - -CRITICAL GUIDELINES: -1. **Tone:** Mentorial, encouraging, but rigorous. Challenge the user to think harder. "That's a good start, but have you considered..." -2. **Conversation:** Be conversational and concise. Don't lecture. -3. **Honesty:** If you don't know, say "I'm not sure about that specific detail." Don't BS. -4. **Utility:** Always try to give something they can use in a round immediately. -5. **Format:** You MUST output valid JSON matching the schema. - -Help them become a better debater, one question at a time.`, -}; - -export function buildGeneratePrompt(input: { - motion: string; - side: string; - format: string; - role: string; - length: string; - audience: string; - tone: string; - language: string; -}): string { - return `Generate a debate case for the following: - -MOTION: ${input.motion} -SIDE: ${input.side} (if "auto", choose the stronger side and explain why) -FORMAT: ${input.format} -ROLE: ${input.role} -LENGTH: ${input.length} -AUDIENCE LEVEL: ${input.audience} -TONE: ${input.tone} -LANGUAGE: ${input.language} - -Provide a complete case structure with arguments, predicted opponent case, and prepared counters. -If any evidence is needed, mark it as needing a source and provide a search query.`; -} - -export function buildAttackPrompt(input: { - caseText: string; - aggressiveness: string; - judgeParadigm: string; - detectFallacies: boolean; - analyzeWeighing: boolean; -}): string { - return `Analyze and attack the following debate case: - ----CASE START--- -${input.caseText} ----CASE END--- - -AGGRESSIVENESS LEVEL: ${input.aggressiveness} -JUDGE PARADIGM: ${input.judgeParadigm} -DETECT FALLACIES: ${input.detectFallacies} -ANALYZE WEIGHING: ${input.analyzeWeighing} - -Identify all weaknesses, provide cross-examination questions, and suggest improvements.`; -} - -export function buildBrainstormPrompt(input: { - motion: string; - side: string; - existingIdeas: string; - constraints: { ethicalRedLines: string[]; allowedSources: string[] }; - outputMode: string; -}): string { - return `Brainstorm arguments for: - -MOTION: ${input.motion} -SIDE TO EXPLORE: ${input.side} -OUTPUT MODE: ${input.outputMode} - -EXISTING IDEAS: -${input.existingIdeas || 'None provided'} - -CONSTRAINTS: -- Ethical red lines: ${input.constraints.ethicalRedLines.join(', ') || 'None specified'} -- Allowed source types: ${input.constraints.allowedSources.join(', ') || 'All'} - -If the input is too vague, ask clarifying questions first.`; -} - -export function buildFactoidsPrompt(input: { - claim: string; - region?: string; - timeframe?: string; - evidenceStrength: string; - language: string; -}): string { - return `Find evidence to support or contextualize: - -CLAIM: ${input.claim} -${input.region ? `REGION: ${input.region}` : ''} -${input.timeframe ? `TIMEFRAME: ${input.timeframe}` : ''} -EVIDENCE STRENGTH PREFERENCE: ${input.evidenceStrength} -LANGUAGE: ${input.language} - -CRITICAL: Only provide citations that you are confident exist. Mark any uncertain sources as "UNVERIFIED".`; -} - -export function buildAskPrompt(input: { - message: string; - mode: string; - citationMode: boolean; - webSearch?: boolean; - conversationHistory: { role: string; content: string }[]; -}): string { - const historyText = input.conversationHistory - .map(m => `${m.role.toUpperCase()}: ${m.content}`) - .join('\n\n'); - - return `${historyText ? `CONVERSATION HISTORY:\n${historyText}\n\n` : ''}USER MESSAGE: ${input.message} - -MODE: ${input.mode} -CITATION MODE: ${input.citationMode ? 'ON - Only make claims you can source' : 'OFF'} -WEB SEARCH: ${input.webSearch ? 'ON' : 'OFF'} - -Respond helpfully while following your coaching guidelines.`; -} diff --git a/src/lib/ai/providers/mock-provider.ts b/src/lib/ai/providers/mock-provider.ts index ff9565a3a..5da759e66 100644 --- a/src/lib/ai/providers/mock-provider.ts +++ b/src/lib/ai/providers/mock-provider.ts @@ -508,5 +508,3 @@ export async function mockJudgeSparringSession(input: SparringScoreInput): Promi } // Names kept short for callers that treat the mock provider as a feature adapter. -export const mockSparringResponse = mockGenerateSparringTurn; -export const mockSparringScore = mockJudgeSparringSession; diff --git a/src/lib/native.ts b/src/lib/native.ts index 76a12c473..df6234ff8 100644 --- a/src/lib/native.ts +++ b/src/lib/native.ts @@ -9,11 +9,6 @@ export function isNativeApp(): boolean { return Capacitor.isNativePlatform(); } -export function getAppRedirectUrl(path = '/app'): string { - if (isNativeApp()) return `${NATIVE_URL_SCHEME}://${path.replace(/^\/+/, '')}`; - return `${window.location.origin}${path}`; -} - export function getWebAppRedirectUrl(path = '/app'): string { const origin = isNativeApp() ? PUBLIC_WEB_APP_ORIGIN : window.location.origin; return `${origin}${path}`; diff --git a/src/lib/validation/url.ts b/src/lib/validation/url.ts index a9126cb09..f33f4eb72 100644 --- a/src/lib/validation/url.ts +++ b/src/lib/validation/url.ts @@ -55,11 +55,3 @@ export function validateUrl(url?: string): string | undefined { return undefined; } } - -/** - * Checks if a URL is safe without normalizing it. - * Useful for validation-only scenarios. - */ -export function isUrlSafe(url?: string): boolean { - return validateUrl(url) !== undefined; -} diff --git a/src/test/example.test.ts b/src/test/example.test.ts deleted file mode 100644 index 52e6d2f0a..000000000 --- a/src/test/example.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from "vitest"; - -describe("example", () => { - it("should pass", () => { - expect(true).toBe(true); - }); -}); diff --git a/src/test/export-utils-extended.test.ts b/src/test/export-utils-extended.test.ts index 86ab03a26..80ae6884c 100644 --- a/src/test/export-utils-extended.test.ts +++ b/src/test/export-utils-extended.test.ts @@ -225,29 +225,4 @@ describe('Export Utils - Extended', () => { }); }); - describe('PDF export functions exist', () => { - it('exportCaseToDocx is a function', () => { - expect(typeof exportCaseToDocx).toBe('function'); - }); - - it('exportAttackToDocx is a function', () => { - expect(typeof exportAttackToDocx).toBe('function'); - }); - - it('exportBrainstormToDocx is a function', () => { - expect(typeof exportBrainstormToDocx).toBe('function'); - }); - - it('exportFactoidsToDocx is a function', () => { - expect(typeof exportFactoidsToDocx).toBe('function'); - }); - - it('exportCaseToGoogleDocs is a function', () => { - expect(typeof exportCaseToGoogleDocs).toBe('function'); - }); - - it('exportBrainstormToGoogleDocs is a function', () => { - expect(typeof exportBrainstormToGoogleDocs).toBe('function'); - }); - }); }); diff --git a/src/test/i18n.test.ts b/src/test/i18n.test.ts index a9c171b86..12979e362 100644 --- a/src/test/i18n.test.ts +++ b/src/test/i18n.test.ts @@ -32,23 +32,6 @@ describe('i18n locale files', () => { const esKeys = flattenKeys(es); const huKeys = flattenKeys(hu); - it('en.json has keys', () => { - expect(enKeys.length).toBeGreaterThan(0); - }); - - it('es.json has keys', () => { - expect(esKeys.length).toBeGreaterThan(0); - }); - - it('hu.json has keys', () => { - expect(huKeys.length).toBeGreaterThan(0); - }); - - it('all 3 locale files have same number of keys', () => { - expect(enKeys.length).toBe(esKeys.length); - expect(enKeys.length).toBe(huKeys.length); - }); - it('all keys in en.json exist in es.json', () => { const esKeySet = new Set(esKeys); const missing = enKeys.filter(k => !esKeySet.has(k)); @@ -100,15 +83,9 @@ describe('i18n locale files', () => { }); describe('i18n runtime', () => { - it('i18n.t returns correct English value for nav.home', () => { + it('i18n.t resolves English nav values', () => { expect(i18n.t('nav.home')).toBe('Home'); - }); - - it('i18n.t returns correct English value for nav.generate', () => { expect(i18n.t('nav.generate')).toBe('Generate'); - }); - - it('i18n.t returns correct English value for nav.settings', () => { expect(i18n.t('nav.settings')).toBe('Settings'); }); @@ -124,11 +101,6 @@ describe('i18n runtime', () => { await i18n.changeLanguage('en'); }); - it('i18n.changeLanguage back to en', async () => { - await i18n.changeLanguage('en'); - expect(i18n.language).toBe('en'); - }); - it('interpolation works for limitModal.limitReachedDesc', () => { const result = i18n.t('limitModal.limitReachedDesc', { current: 5, limit: 10 }); expect(result).toContain('5'); diff --git a/src/test/pages/HistoryPage.test.tsx b/src/test/pages/HistoryPage.test.tsx index 4d081f43c..b5c0fcd73 100644 --- a/src/test/pages/HistoryPage.test.tsx +++ b/src/test/pages/HistoryPage.test.tsx @@ -114,9 +114,4 @@ describe('HistoryPage', () => { const { container } = render(); expect(container.querySelector('.max-w-5xl')).not.toBeNull(); }); - - it('renders without crashing with empty arrays', () => { - const { container } = render(); - expect(container).toBeDefined(); - }); }); diff --git a/src/test/pages/pages.test.tsx b/src/test/pages/pages.test.tsx index db4fb0f35..dd4866512 100644 --- a/src/test/pages/pages.test.tsx +++ b/src/test/pages/pages.test.tsx @@ -51,11 +51,6 @@ const renderWithRouter = (component: React.ReactNode, route = '/') => { describe('Pages', () => { describe('LandingPage', () => { - it('renders without crashing', () => { - renderWithRouter(); - expect(document.body).toBeDefined(); - }); - it('contains hero section', () => { renderWithRouter(); expect(screen.getByRole('heading', { level: 1 })).toBeDefined(); @@ -70,11 +65,6 @@ describe('Pages', () => { renderWithRouter(); expect(screen.getAllByRole('heading').length).toBeGreaterThan(1); }); - - it('contains footer', () => { - renderWithRouter(); - expect(screen.getAllByText(/DebateLab/i).length).toBeGreaterThan(0); - }); }); describe('HomePage', () => { @@ -104,10 +94,5 @@ describe('Pages', () => { renderWithRouter(); expect(screen.getByText('404')).toBeDefined(); }); - - it('renders without crashing', () => { - renderWithRouter(); - expect(document.body).toBeDefined(); - }); }); }); diff --git a/src/test/providers.test.ts b/src/test/providers.test.ts index 17e746769..80e2cfabd 100644 --- a/src/test/providers.test.ts +++ b/src/test/providers.test.ts @@ -2,90 +2,10 @@ import { describe, it, expect } from 'vitest'; import { DEFAULT_PROVIDERS } from '@/lib/ai/providers/types'; describe('DEFAULT_PROVIDERS', () => { - it('has 3 providers', () => { - expect(DEFAULT_PROVIDERS.length).toBe(3); - }); - - it('has openai provider', () => { - expect(DEFAULT_PROVIDERS.find(p => p.id === 'openai')).toBeDefined(); - }); - - it('has anthropic provider', () => { - expect(DEFAULT_PROVIDERS.find(p => p.id === 'anthropic')).toBeDefined(); - }); - - it('has google provider', () => { - expect(DEFAULT_PROVIDERS.find(p => p.id === 'google')).toBeDefined(); - }); - - it('each provider has a name', () => { - DEFAULT_PROVIDERS.forEach(p => { - expect(p.name).toBeTruthy(); - }); - }); - - it('each provider has models array', () => { - DEFAULT_PROVIDERS.forEach(p => { - expect(Array.isArray(p.models)).toBe(true); - expect(p.models.length).toBeGreaterThan(0); - }); - }); - - it('all models have id field', () => { - DEFAULT_PROVIDERS.forEach(p => { - p.models.forEach(m => { - expect(m.id).toBeTruthy(); - }); - }); - }); - - it('all models have name field', () => { - DEFAULT_PROVIDERS.forEach(p => { - p.models.forEach(m => { - expect(m.name).toBeTruthy(); - }); - }); - }); - - it('all models have maxTokens field', () => { - DEFAULT_PROVIDERS.forEach(p => { - p.models.forEach(m => { - expect(m.maxTokens).toBeGreaterThan(0); - }); - }); - }); - - it('Pro models are correctly marked with isProOnly', () => { - const proModels = DEFAULT_PROVIDERS.flatMap(p => p.models).filter(m => m.isProOnly); - expect(proModels.length).toBeGreaterThan(0); - proModels.forEach(m => { - expect(m.isProOnly).toBe(true); - }); - }); - - it('non-Pro models exist', () => { - const freeModels = DEFAULT_PROVIDERS.flatMap(p => p.models).filter(m => !m.isProOnly); - expect(freeModels.length).toBeGreaterThan(0); - }); - + // Model IDs key request routing and the stored per-session model choice, so a + // collision across providers would silently route to the wrong provider. it('no duplicate model IDs across all providers', () => { const allIds = DEFAULT_PROVIDERS.flatMap(p => p.models.map(m => m.id)); - const uniqueIds = new Set(allIds); - expect(uniqueIds.size).toBe(allIds.length); - }); - - it('provider isAvailable returns true for openai', () => { - const openai = DEFAULT_PROVIDERS.find(p => p.id === 'openai')!; - expect(openai.isAvailable()).toBe(true); - }); - - it('provider isAvailable returns true for anthropic', () => { - const anthropic = DEFAULT_PROVIDERS.find(p => p.id === 'anthropic')!; - expect(anthropic.isAvailable()).toBe(true); - }); - - it('provider isAvailable returns true for google', () => { - const google = DEFAULT_PROVIDERS.find(p => p.id === 'google')!; - expect(google.isAvailable()).toBe(true); + expect(new Set(allIds).size).toBe(allIds.length); }); }); diff --git a/src/test/stores/settings-store-branches.test.ts b/src/test/stores/settings-store-branches.test.ts index 63840a1e3..4b134a48d 100644 --- a/src/test/stores/settings-store-branches.test.ts +++ b/src/test/stores/settings-store-branches.test.ts @@ -48,9 +48,6 @@ describe('settings-store branch coverage', () => { const afterRehydrate = onRehydrateStorage?.(useSettingsStore.getState()); if (!afterRehydrate) throw new Error('Expected a rehydration callback'); - // Covers the `if (!state) return` branch. - afterRehydrate(undefined as any); - const partialState = { featureConfigs: undefined, language: '', diff --git a/src/test/utils.test.ts b/src/test/utils.test.ts deleted file mode 100644 index 667ec87bf..000000000 --- a/src/test/utils.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -// src/test/utils.test.ts -import { describe, it, expect } from 'vitest'; -import { cn } from '@/lib/utils'; - -describe('Utility Functions', () => { - describe('cn (class name merger)', () => { - it('merges multiple class strings', () => { - const result = cn('class1', 'class2'); - expect(result).toContain('class1'); - expect(result).toContain('class2'); - }); - - it('handles undefined values', () => { - const result = cn('class1', undefined, 'class2'); - expect(result).toContain('class1'); - expect(result).toContain('class2'); - expect(result).not.toContain('undefined'); - }); - - it('handles null values', () => { - const result = cn('class1', null, 'class2'); - expect(result).toContain('class1'); - expect(result).toContain('class2'); - }); - - it('handles boolean conditional classes', () => { - const isActive = true; - const result = cn('base', isActive && 'active'); - expect(result).toContain('base'); - expect(result).toContain('active'); - }); - - it('excludes false conditional classes', () => { - const isActive = false; - const result = cn('base', isActive && 'active'); - expect(result).toContain('base'); - expect(result).not.toContain('active'); - }); - - it('handles empty string', () => { - const result = cn(''); - expect(result).toBe(''); - }); - - it('handles no arguments', () => { - const result = cn(); - expect(result).toBe(''); - }); - - it('merges tailwind conflicting classes correctly', () => { - // tailwind-merge should resolve conflicts, preferring the last class - const result = cn('px-2', 'px-4'); - expect(result).toContain('px-4'); - // The conflicting px-2 should be removed - expect(result).not.toContain('px-2'); - }); - - it('handles object syntax', () => { - const result = cn({ 'bg-red-500': true, 'bg-blue-500': false }); - expect(result).toContain('bg-red-500'); - expect(result).not.toContain('bg-blue-500'); - }); - - it('handles array of classes', () => { - const result = cn(['class1', 'class2']); - expect(result).toContain('class1'); - expect(result).toContain('class2'); - }); - - it('handles mixed arguments', () => { - const showTrue = true; - const showFalse = false; - const result = cn( - 'base-class', - showTrue && 'conditional-true', - showFalse && 'conditional-false', - { 'object-true': true, 'object-false': false }, - undefined, - null, - 'final-class' - ); - expect(result).toContain('base-class'); - expect(result).toContain('conditional-true'); - expect(result).not.toContain('conditional-false'); - expect(result).toContain('object-true'); - expect(result).not.toContain('object-false'); - expect(result).toContain('final-class'); - }); - }); -}); diff --git a/src/test/validation.test.ts b/src/test/validation.test.ts index d2ccace23..f90b9b687 100644 --- a/src/test/validation.test.ts +++ b/src/test/validation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { validateUrl, isUrlSafe } from '@/lib/validation/url'; +import { validateUrl } from '@/lib/validation/url'; import { validateFileUpload, validateSingleFile, MAX_FILE_SIZE, MAX_FILE_COUNT } from '@/lib/validation/file'; describe('URL Validation', () => { @@ -38,18 +38,6 @@ describe('URL Validation', () => { expect(validateUrl(' https://example.com ')).toBe('https://example.com/'); }); }); - - describe('isUrlSafe', () => { - it('returns true for safe URLs', () => { - expect(isUrlSafe('https://example.com')).toBe(true); - expect(isUrlSafe('http://example.com')).toBe(true); - }); - - it('returns false for dangerous URLs', () => { - expect(isUrlSafe('javascript:alert(1)')).toBe(false); - expect(isUrlSafe('data:text/html,test')).toBe(false); - }); - }); }); describe('File Validation', () => { diff --git a/supabase/functions/_shared/auth.ts b/supabase/functions/_shared/auth.ts index 7e5cba11a..4ccee510d 100644 --- a/supabase/functions/_shared/auth.ts +++ b/supabase/functions/_shared/auth.ts @@ -71,21 +71,6 @@ export async function requireUser(req: Request): Promise { }; } -export async function requireAdmin(req: Request): Promise { - const context = await requireUser(req); - const { data, error } = await context.adminClient - .from("user_roles") - .select("role") - .eq("user_id", context.user.id) - .maybeSingle(); - - if (error || data?.role !== "admin") { - throw new HttpError(403, "Admin access required"); - } - - return context; -} - export function jsonError(error: unknown, corsHeaders: Record) { const status = error instanceof HttpError ? error.status : 500; const message = error instanceof Error ? error.message : "Internal server error"; diff --git a/supabase/functions/_shared/source-quality.ts b/supabase/functions/_shared/source-quality.ts index 37b1e8e01..603e0c50d 100644 --- a/supabase/functions/_shared/source-quality.ts +++ b/supabase/functions/_shared/source-quality.ts @@ -383,10 +383,6 @@ export function buildSearchRequests(claim: string, region = "", preferredSourceT return deduplicated.slice(0, 6); } -export function buildSearchQueries(claim: string, region = "", preferredSourceType = "any", timeframe = ""): string[] { - return buildSearchRequests(claim, region, preferredSourceType, timeframe).map((request) => request.query); -} - // Bounded per-result snippet: Exa may return up to 10k characters per result, // which would bloat prompts when several results are included. const MAX_RESULT_SNIPPET_CHARS = 1200; diff --git a/supabase/functions/ai/source-quality.test.ts b/supabase/functions/ai/source-quality.test.ts index 965a95f42..50f0aae04 100644 --- a/supabase/functions/ai/source-quality.test.ts +++ b/supabase/functions/ai/source-quality.test.ts @@ -1,4 +1,4 @@ -import { buildSearchQueries, buildSearchRequests, rankSearchResults } from "../_shared/source-quality.ts"; +import { buildSearchRequests, rankSearchResults } from "../_shared/source-quality.ts"; function assert(condition: boolean, message: string) { if (!condition) throw new Error(message); @@ -63,13 +63,13 @@ Deno.test("search query builder adds debate tab queries for tournament claims", }); Deno.test("search query builder adds policy and statistics source queries", () => { - const queries = buildSearchQueries("solar energy costs and inflation policy", "United States", "any", "2024 2025"); + const queries = buildSearchRequests("solar energy costs and inflation policy", "United States", "any", "2024 2025").map((request) => request.query); assert(queries.some((query) => /World Bank|OECD|IMF|UN/i.test(query)), `Expected intergovernmental source query, got ${queries.join("; ")}`); assert(queries.some((query) => /government statistics/i.test(query)), `Expected government statistics query, got ${queries.join("; ")}`); }); Deno.test("search query builder adds legal primary-source queries", () => { - const queries = buildSearchQueries("new court ruling on social media regulation", "European Union"); + const queries = buildSearchRequests("new court ruling on social media regulation", "European Union").map((request) => request.query); assert(queries.some((query) => /official law court/i.test(query)), `Expected official legal source query, got ${queries.join("; ")}`); assert(queries.some((query) => /primary source/i.test(query)), `Expected primary source query, got ${queries.join("; ")}`); }); From fb5fb8f81de3aaa8513f303c7b173900cb1dbe24 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Sep 2026 22:41:52 +0000 Subject: [PATCH 2/2] test: give CI expect assertions room for lazy route chunks The e2e suite runs against the Vite dev server, which transforms lazily-imported route modules on demand. The first assertion after navigating to a lazy route therefore races that transform waterfall. Trace timings from the failing CI run on this branch: 22:16:35.139 GET /app/sparring (document) 22:16:38.463 SparringPage.tsx (+3.3s) 22:16:38.755 SparringSetup.tsx (+3.6s, owns the heading) Against the 5s Playwright default, with two workers sharing a runner, that overruns. The failure screenshot shows the heading rendered correctly -- the budget simply expired first. Raise the expect timeout to 15s under CI only, leaving local runs at the 5s default so genuine hangs still surface quickly. No assertion is weakened: every expectation still has to pass, it just gets a budget that reflects what the dev server actually costs on a shared runner. Pre-existing, not caused by the dead-code removal in this branch: PR 28 recorded the same test as flaky, and it reproduces on base 79b65ca with these changes absent. Verified under 12 busy-loop jobs on 8 cores, 18-test sample: 5s (control): 1 flaky, 17 passed 15s (fixed): 18 passed Full suite on a clean machine: 124 passed, 0 flaky. --- playwright.config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/playwright.config.ts b/playwright.config.ts index 605f4ca54..49742af09 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -28,6 +28,11 @@ export default defineConfig({ retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 2 : undefined, reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list', + // The dev server transforms lazily-imported route modules on demand, so the + // first assertion after navigating to a lazy route races that waterfall. On a + // shared CI runner it has measured over 3.5s from document to the component + // that owns the heading, which overruns the 5s default. + expect: { timeout: process.env.CI ? 15_000 : 5_000 }, use: { baseURL, trace: 'retain-on-failure',