test: add checkAnswer edge case tests with findings (15 cases)

Documents: unicode accent stripping not supported, 3-char reverse
containment false positives, first-match-not-best-match ordering,
yes/true equivalence in boolean checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 00:12:49 +00:00
co-authored by Claude Opus 4.6
parent 8b72bef22e
commit 39b8504157
+97
View File
@@ -0,0 +1,97 @@
import { describe, it, expect } from 'vitest'
import { checkAnswer } from './answers.js'
describe('checkAnswer edge cases', () => {
it('unicode answers — exact accented match works', () => {
expect(checkAnswer('café', ['café'])).toBeGreaterThanOrEqual(0.9)
})
// FINDING: accent-stripped matching NOT supported — "cafe" != "café"
it('unicode — accent stripping not supported (known limitation)', () => {
expect(checkAnswer('cafe', ['café'])).toBe(0)
})
it('special characters in answers', () => {
expect(checkAnswer('C++', ['C++'])).toBeGreaterThanOrEqual(0.9)
expect(checkAnswer('c++', ['C++'])).toBeGreaterThanOrEqual(0.9)
expect(checkAnswer('AT&T', ['AT&T'])).toBeGreaterThanOrEqual(0.9)
})
it('numeric word forms vs digits', () => {
expect(checkAnswer('42', ['forty-two'])).toBeGreaterThanOrEqual(0.8)
expect(checkAnswer('forty-two', ['42'])).toBeGreaterThanOrEqual(0.8)
expect(checkAnswer('twenty one', ['21'])).toBeGreaterThanOrEqual(0.8)
expect(checkAnswer('21', ['twenty-one'])).toBeGreaterThanOrEqual(0.8)
})
it('extra whitespace handling', () => {
expect(checkAnswer(' Bitcoin ', ['Bitcoin'])).toBeGreaterThanOrEqual(0.9)
expect(checkAnswer('Bitcoin\t\n', ['Bitcoin'])).toBeGreaterThanOrEqual(0.9)
expect(checkAnswer(' ', ['Bitcoin'])).toBe(0)
})
it('substring of correct answer — reverse containment', () => {
// FINDING: "Bit" (3 chars) matches "Bitcoin" via reverse containment (answer contains response)
// This is a known false positive for short substrings at >= 3 chars
expect(checkAnswer('Bit', ['Bitcoin'])).toBe(0.8)
expect(checkAnswer('Bitcoin', ['Bitcoin Core'])).toBeGreaterThanOrEqual(0.8)
})
it('answer containing correct plus extra text', () => {
expect(checkAnswer('The answer is Bitcoin obviously', ['Bitcoin'])).toBeGreaterThanOrEqual(0.8)
expect(checkAnswer('I think it is 42 satoshis', ['42'])).toBeGreaterThanOrEqual(0.8)
})
it('empty and null inputs', () => {
expect(checkAnswer(null, ['Bitcoin'])).toBe(0)
expect(checkAnswer('', ['Bitcoin'])).toBe(0)
expect(checkAnswer('Bitcoin', [])).toBe(0)
})
it('case variations', () => {
expect(checkAnswer('BITCOIN', ['bitcoin'])).toBeGreaterThanOrEqual(0.9)
expect(checkAnswer('bItCoIn', ['Bitcoin'])).toBeGreaterThanOrEqual(0.9)
})
it('plural/singular matching', () => {
expect(checkAnswer('blocks', ['block'])).toBeGreaterThanOrEqual(0.8)
expect(checkAnswer('node', ['nodes'])).toBeGreaterThanOrEqual(0.8)
expect(checkAnswer('halving', ['halvings'])).toBeGreaterThanOrEqual(0.8)
})
it('contraction matching', () => {
expect(checkAnswer("can't", ['cannot'])).toBeGreaterThanOrEqual(0.8)
expect(checkAnswer("don't", ['do not'])).toBeGreaterThanOrEqual(0.8)
})
it('multiple valid answers — returns max confidence across all', () => {
// FINDING: checkAnswer iterates answers in order, returns first match
// "Satoshi" matches "Satoshi Nakamoto" first via reverse containment (0.8)
// rather than exact-matching "Satoshi" (1.0). Returns first, not best.
const score = checkAnswer('Satoshi', ['Satoshi Nakamoto', 'Satoshi'])
expect(score).toBeGreaterThanOrEqual(0.8) // gets 0.8 from first match
expect(checkAnswer('Nakamoto', ['Satoshi Nakamoto', 'Satoshi'])).toBeGreaterThanOrEqual(0.8)
})
it('adversarial input — very long string', () => {
const longStr = 'a'.repeat(10000) + ' Bitcoin ' + 'b'.repeat(10000)
// Should still find the answer without catastrophic backtracking
const start = performance.now()
checkAnswer(longStr, ['Bitcoin'])
const elapsed = performance.now() - start
expect(elapsed).toBeLessThan(50) // should be very fast
})
it('true/false matching', () => {
expect(checkAnswer('true', ['True'])).toBeGreaterThanOrEqual(0.9)
// FINDING: "yes" matches "True" because true/false keyword check is broad
// Treats yes/true as equivalent. Not necessarily wrong for trivia.
expect(checkAnswer('yes', ['True'])).toBe(0.9)
expect(checkAnswer('false', ['False'])).toBeGreaterThanOrEqual(0.9)
})
it('boolean-like answers', () => {
expect(checkAnswer('True', ['true'])).toBeGreaterThanOrEqual(0.9)
expect(checkAnswer('FALSE', ['false'])).toBeGreaterThanOrEqual(0.9)
})
})