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:
co-authored by
Claude Opus 4.6
parent
8b72bef22e
commit
39b8504157
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user