import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { isQuestionHardFilteredAfterAnswers } from '@/lib/gekanatorQuestionFilters' import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorQuestion, GekanatorQuestionCondition, } from '@/lib/gekanator' const question = ( condition: GekanatorQuestionCondition, ): GekanatorQuestion => ({ id: `${ condition.type }:candidate`, text: 'candidate?', kind: condition.type === 'source' ? 'source' : condition.type.startsWith ('original-') ? 'original_date' : condition.type.startsWith ('title-') ? 'title' : 'tag', condition, source: 'default', priorityWeight: 1, test: () => false, }) const answer = ( condition: GekanatorQuestionCondition, value: GekanatorAnswerValue, ): GekanatorAnswerLog => ({ questionId: 'previous', questionText: 'previous?', questionCondition: condition, answer: value, originalAnswer: value, }) const blocked = ( candidate: GekanatorQuestionCondition, previous: GekanatorQuestionCondition, value: GekanatorAnswerValue, ): boolean => isQuestionHardFilteredAfterAnswers (question (candidate), [answer (previous, value)]) const gekanatorPageSource = readFileSync ( resolve (process.cwd (), 'src/pages/GekanatorPage.tsx'), 'utf8') const gekanatorBackdropSource = gekanatorPageSource.slice ( gekanatorPageSource.indexOf ('const GekanatorBackdrop'), gekanatorPageSource.indexOf ('const expectedAnswerFor')) const gekanatorChooseQuestionSource = gekanatorPageSource.slice ( gekanatorPageSource.indexOf ('const chooseQuestion'), gekanatorPageSource.indexOf ('const winningRunPriorityFor')) const gekanatorFallbackQuestionSource = gekanatorPageSource.slice ( gekanatorPageSource.indexOf ('const chooseFallbackQuestion'), gekanatorPageSource.indexOf ('const shouldEnterGuessPhase')) describe('GekanatorBackdrop regression structure', () => { it('keeps displayedBackdropMode as the render-time source of truth', () => { expect(gekanatorBackdropSource).not.toContain ('isLeavingGuessBackdrop') expect(gekanatorBackdropSource).not.toContain ('renderBackdropMode') expect(gekanatorBackdropSource).not.toContain ('renderWinningRunCount') expect(gekanatorBackdropSource).not.toContain ('renderThumbnails') expect(gekanatorBackdropSource).not.toContain ('renderIsCrossfading') expect(gekanatorBackdropSource).toContain ( "const renderedSettings = settingsForMode (displayedBackdropMode)") expect(gekanatorBackdropSource).toContain ( 'scaleForMode (displayedBackdropMode, displayedWinningRunCount)') expect(gekanatorBackdropSource).toContain ( "backdropMode === 'guess' || displayedBackdropMode === 'guess'") }) it('does not split guess into a separate renderer or force a remount', () => { expect(gekanatorBackdropSource).not.toContain ('renderStaticGuessBackdrop') expect(gekanatorBackdropSource).not.toContain ('guessZoomAnimationKey') expect(gekanatorBackdropSource).not.toContain ('shouldAnimateGuessZoomIn') expect(gekanatorBackdropSource).not.toContain ('previousBackdropModeRef') expect(gekanatorBackdropSource).not.toContain ( 'if (isGuessPresentation && guessThumbnail)') }) it('keeps tile keys independent from backdrop mode', () => { expect(gekanatorBackdropSource).toContain ('key={duplicate}') expect(gekanatorBackdropSource).toContain ('key={`${ duplicate }:${ index }`}') expect(gekanatorBackdropSource).not.toMatch (/key=\{`\$\{\s*mode/) expect(gekanatorBackdropSource).not.toMatch (/key=\{`\$\{\s*displayedBackdropMode/) }) it('keeps guess on the shared scale, x, and y animation path', () => { expect(gekanatorBackdropSource).toContain ('animate={{ scale: renderedScale') expect(gekanatorBackdropSource).toContain ( "x: displayedBackdropMode === 'guess' ? guessFocusOffset.x : '0%'") expect(gekanatorBackdropSource).toContain ( "y: displayedBackdropMode === 'guess' ? guessFocusOffset.y : '0%'") }) }) describe('Gekanator question selection regression structure', () => { it('prefers normal questions after user_suggested quota has been met', () => { const normalFallbackIndex = gekanatorChooseQuestionSource.indexOf ( 'else if (normalPool.length > 0)') const effectiveFallbackIndex = gekanatorChooseQuestionSource.indexOf ( 'else if (effectiveUserSuggestedPool.length > 0)') expect(normalFallbackIndex).toBeGreaterThan(0) expect(effectiveFallbackIndex).toBeGreaterThan(0) expect(normalFallbackIndex).toBeLessThan(effectiveFallbackIndex) }) it('does not let fallback questions bypass user_suggested purpose tracking', () => { expect(gekanatorFallbackQuestionSource).toContain ( "question.source !== 'user_suggested'") }) it('does not show a fixed extra-question count in the extra learning UI', () => { expect(gekanatorPageSource).not.toContain ('追加で 2 問まで答えてください。') expect(gekanatorPageSource).toContain ('追加で質問に答えてください。') }) }) describe('isQuestionHardFilteredAfterAnswers', () => { it('blocks only contradictory or redundant month questions after a yes answer', () => { const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 } expect(blocked ({ type: 'original-month', month: 12 }, previous, 'yes')).toBe(true) expect(blocked ({ type: 'original-month', month: 2 }, previous, 'yes')).toBe(true) expect(blocked ({ type: 'original-month-day', monthDay: '2-14' }, previous, 'yes')) .toBe(true) expect(blocked ({ type: 'original-month-day', monthDay: '12-25' }, previous, 'yes')) .toBe(false) expect(blocked ({ type: 'original-year', year: 2024 }, previous, 'yes')).toBe(false) expect(blocked ({ type: 'source', host: 'example.com' }, previous, 'yes')).toBe(false) expect(blocked ({ type: 'tag', key: 'character:喜多郁代' }, previous, 'yes')).toBe(false) }) it('blocks same-month facts after a no answer', () => { const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 } expect(blocked ({ type: 'original-month', month: 12 }, previous, 'no')).toBe(true) expect(blocked ({ type: 'original-month', month: 2 }, previous, 'no')).toBe(false) expect(blocked ({ type: 'original-month-day', monthDay: '12-25' }, previous, 'no')) .toBe(true) expect(blocked ({ type: 'original-month-day', monthDay: '2-14' }, previous, 'no')) .toBe(false) }) it('blocks all month and month-day questions after a month-day yes answer', () => { const previous: GekanatorQuestionCondition = { type: 'original-month-day', monthDay: '12-25', } expect(blocked ({ type: 'original-month', month: 12 }, previous, 'yes')).toBe(true) expect(blocked ({ type: 'original-month', month: 2 }, previous, 'yes')).toBe(true) expect(blocked ({ type: 'original-month-day', monthDay: '12-25' }, previous, 'yes')) .toBe(true) expect(blocked ({ type: 'original-month-day', monthDay: '12-26' }, previous, 'yes')) .toBe(true) }) it('blocks the same month-day only after a month-day no answer', () => { const previous: GekanatorQuestionCondition = { type: 'original-month-day', monthDay: '12-25', } expect(blocked ({ type: 'original-month-day', monthDay: '12-25' }, previous, 'no')) .toBe(true) expect(blocked ({ type: 'original-month-day', monthDay: '12-26' }, previous, 'no')) .toBe(false) expect(blocked ({ type: 'original-month', month: 12 }, previous, 'no')).toBe(false) }) it('blocks year and source as single-value facts', () => { expect(blocked ( { type: 'original-year', year: 2025 }, { type: 'original-year', year: 2024 }, 'yes', )).toBe(true) expect(blocked ( { type: 'original-year', year: 2024 }, { type: 'original-year', year: 2024 }, 'no', )).toBe(true) expect(blocked ( { type: 'source', host: 'b.example' }, { type: 'source', host: 'a.example' }, 'yes', )).toBe(true) expect(blocked ( { type: 'source', host: 'b.example' }, { type: 'source', host: 'a.example' }, 'no', )).toBe(false) }) it('does not hard-filter partial, probably_no, or unknown fact answers', () => { const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 } const candidate: GekanatorQuestionCondition = { type: 'original-month', month: 2 } expect(blocked (candidate, previous, 'partial')).toBe(false) expect(blocked (candidate, previous, 'probably_no')).toBe(false) expect(blocked (candidate, previous, 'unknown')).toBe(false) }) it('keeps title-length hard redundancy for yes and no only', () => { const previous: GekanatorQuestionCondition = { type: 'title-length-at-least', length: 30, } expect(blocked ({ type: 'title-length-at-least', length: 20 }, previous, 'yes')) .toBe(true) expect(blocked ({ type: 'title-length-at-least', length: 40 }, previous, 'yes')) .toBe(false) expect(blocked ({ type: 'title-length-at-least', length: 40 }, previous, 'no')) .toBe(true) expect(blocked ({ type: 'title-length-at-least', length: 20 }, previous, 'no')) .toBe(false) expect(blocked ({ type: 'title-length-at-least', length: 20 }, previous, 'partial')) .toBe(false) }) })