def6870f06
Reviewed-on: #365 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
376 行
9.6 KiB
TypeScript
376 行
9.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { apiPost } from '@/lib/api'
|
|
import {
|
|
buildGekanatorQuestions,
|
|
expectedAnswerForQuestion,
|
|
restoreGekanatorQuestion,
|
|
saveGekanatorExtraQuestionAnswers,
|
|
saveGekanatorGame,
|
|
saveGekanatorQuestionSuggestion,
|
|
} from '@/lib/gekanator'
|
|
|
|
import type {
|
|
GekanatorAnswerLog,
|
|
StoredGekanatorQuestion,
|
|
} from '@/lib/gekanator'
|
|
import type { Post } from '@/types'
|
|
|
|
vi.mock('@/lib/api', () => ({
|
|
apiGet: vi.fn(),
|
|
apiPost: vi.fn(),
|
|
}))
|
|
|
|
const mockedApiPost = vi.mocked(apiPost)
|
|
|
|
const post = (overrides: Partial<Post> = {}): Post => ({
|
|
id: 1,
|
|
versionNo: 1,
|
|
url: 'https://example.com/posts/1',
|
|
title: 'post title',
|
|
thumbnail: null,
|
|
thumbnailBase: null,
|
|
tags: [],
|
|
viewed: false,
|
|
related: [],
|
|
originalCreatedFrom: null,
|
|
originalCreatedBefore: null,
|
|
createdAt: '2026-06-10T00:00:00.000Z',
|
|
updatedAt: '2026-06-10T00:00:00.000Z',
|
|
uploadedUser: null,
|
|
...overrides,
|
|
})
|
|
|
|
describe('expectedAnswerForQuestion', () => {
|
|
it('returns a direct example answer when present', () => {
|
|
const question: StoredGekanatorQuestion = {
|
|
id: 'post-similarity:10',
|
|
text: '喜多ちゃんが泣いてる?',
|
|
kind: 'post_similarity',
|
|
source: 'user_suggested',
|
|
priorityWeight: 1.2,
|
|
condition: {
|
|
type: 'post-similarity',
|
|
postId: 999,
|
|
answer: 'yes',
|
|
threshold: 0.65,
|
|
},
|
|
exampleAnswers: {
|
|
1: 'partial',
|
|
},
|
|
}
|
|
|
|
expect(expectedAnswerForQuestion(question, post({ id: 1 }))).toBe('partial')
|
|
})
|
|
|
|
it('returns the condition answer for the original post_similarity post', () => {
|
|
const question: StoredGekanatorQuestion = {
|
|
id: 'post-similarity:10',
|
|
text: '喜多ちゃんが泣いてる?',
|
|
kind: 'post_similarity',
|
|
source: 'user_suggested',
|
|
priorityWeight: 1.2,
|
|
condition: {
|
|
type: 'post-similarity',
|
|
postId: 123,
|
|
answer: 'probably_no',
|
|
threshold: 0.65,
|
|
},
|
|
}
|
|
|
|
expect(expectedAnswerForQuestion(question, post({ id: 123 }))).toBe('probably_no')
|
|
})
|
|
|
|
it('returns null for an unrelated post_similarity post without examples', () => {
|
|
const question: StoredGekanatorQuestion = {
|
|
id: 'post-similarity:10',
|
|
text: '喜多ちゃんが泣いてる?',
|
|
kind: 'post_similarity',
|
|
source: 'user_suggested',
|
|
priorityWeight: 1.2,
|
|
condition: {
|
|
type: 'post-similarity',
|
|
postId: 123,
|
|
answer: 'yes',
|
|
threshold: 0.65,
|
|
},
|
|
}
|
|
|
|
expect(expectedAnswerForQuestion(question, post({ id: 456 }))).toBeNull()
|
|
})
|
|
|
|
it('returns yes for a matching tag question', () => {
|
|
const question: StoredGekanatorQuestion = {
|
|
id: 'tag:character:喜多郁代',
|
|
text: '喜多ちゃんが関係してる?',
|
|
kind: 'tag',
|
|
condition: {
|
|
type: 'tag',
|
|
key: 'character:喜多郁代',
|
|
},
|
|
}
|
|
|
|
expect(
|
|
expectedAnswerForQuestion(
|
|
question,
|
|
post({
|
|
tags: [
|
|
{
|
|
id: 1,
|
|
name: '喜多郁代',
|
|
category: 'character',
|
|
aliases: [],
|
|
parents: [],
|
|
postCount: 1,
|
|
createdAt: '2026-06-10T00:00:00.000Z',
|
|
updatedAt: '2026-06-10T00:00:00.000Z',
|
|
hasWiki: false,
|
|
hasDeerjikists: false,
|
|
materialId: null,
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
).toBe('yes')
|
|
})
|
|
|
|
it('returns no for a non-matching tag question', () => {
|
|
const question: StoredGekanatorQuestion = {
|
|
id: 'tag:character:喜多郁代',
|
|
text: '喜多ちゃんが関係してる?',
|
|
kind: 'tag',
|
|
condition: {
|
|
type: 'tag',
|
|
key: 'character:喜多郁代',
|
|
},
|
|
}
|
|
|
|
expect(expectedAnswerForQuestion(question, post({ tags: [] }))).toBe('no')
|
|
})
|
|
|
|
it('ignores example answers for direct title facts', () => {
|
|
const question: StoredGekanatorQuestion = {
|
|
id: 'title:length-at-least:20',
|
|
text: 'タイトルは 20 文字以上?',
|
|
kind: 'title',
|
|
condition: {
|
|
type: 'title-length-at-least',
|
|
length: 20,
|
|
},
|
|
exampleAnswers: {
|
|
1: 'yes',
|
|
},
|
|
}
|
|
|
|
expect(expectedAnswerForQuestion(question, post({ id: 1, title: 'short' }))).toBe('no')
|
|
})
|
|
})
|
|
|
|
describe('restoreGekanatorQuestion', () => {
|
|
it('uses default source and priority weight when omitted', () => {
|
|
const question = restoreGekanatorQuestion({
|
|
id: 'tag:character:喜多郁代',
|
|
text: '喜多ちゃんが関係してる?',
|
|
kind: 'tag',
|
|
condition: {
|
|
type: 'tag',
|
|
key: 'character:喜多郁代',
|
|
},
|
|
})
|
|
|
|
expect(question.source).toBe('default')
|
|
expect(question.priorityWeight).toBe(1)
|
|
})
|
|
|
|
it('tests a post_similarity question using direct examples', () => {
|
|
const question = restoreGekanatorQuestion({
|
|
id: 'post-similarity:10',
|
|
text: '喜多ちゃんが泣いてる?',
|
|
kind: 'post_similarity',
|
|
source: 'user_suggested',
|
|
priorityWeight: 1.2,
|
|
condition: {
|
|
type: 'post-similarity',
|
|
postId: 999,
|
|
answer: 'yes',
|
|
threshold: 0.65,
|
|
},
|
|
exampleAnswers: {
|
|
1: 'yes',
|
|
2: 'no',
|
|
},
|
|
})
|
|
|
|
expect(question.test(post({ id: 1 }))).toBe(true)
|
|
expect(question.test(post({ id: 2 }))).toBe(false)
|
|
expect(question.test(post({ id: 3 }))).toBe(false)
|
|
})
|
|
|
|
it('tests a post_similarity question against its configured partial answer', () => {
|
|
const question = restoreGekanatorQuestion({
|
|
id: 'post-similarity:10',
|
|
text: '喜多ちゃんが泣いてる?',
|
|
kind: 'post_similarity',
|
|
source: 'user_suggested',
|
|
priorityWeight: 1.2,
|
|
condition: {
|
|
type: 'post-similarity',
|
|
postId: 999,
|
|
answer: 'partial',
|
|
threshold: 0.65,
|
|
},
|
|
exampleAnswers: {
|
|
1: 'partial',
|
|
2: 'yes',
|
|
},
|
|
})
|
|
|
|
expect(question.test(post({ id: 1 }))).toBe(true)
|
|
expect(question.test(post({ id: 2 }))).toBe(false)
|
|
})
|
|
|
|
it('normalizes legacy title-length-greater-than questions', () => {
|
|
const question = restoreGekanatorQuestion({
|
|
id: 'title:length-greater-than:20',
|
|
text: '題名が長めの投稿?',
|
|
kind: 'title',
|
|
condition: {
|
|
type: 'title-length-greater-than',
|
|
length: 20,
|
|
},
|
|
})
|
|
|
|
expect(question.id).toBe('title:length-at-least:21')
|
|
expect(question.condition).toEqual({
|
|
type: 'title-length-at-least',
|
|
length: 21,
|
|
})
|
|
expect(question.test(post({ title: 'x'.repeat(20) }))).toBe(false)
|
|
expect(question.test(post({ title: 'x'.repeat(21) }))).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('buildGekanatorQuestions', () => {
|
|
it('builds quantitative title length questions', () => {
|
|
const questions = buildGekanatorQuestions([
|
|
post({ id: 1, title: 'a' }),
|
|
post({ id: 2, title: 'bb' }),
|
|
post({ id: 3, title: 'ccc' }),
|
|
post({ id: 4, title: 'dddd' }),
|
|
])
|
|
const titleQuestion = questions.find(question =>
|
|
question.condition.type === 'title-length-at-least')
|
|
|
|
expect(titleQuestion?.text).toMatch(/^タイトルは \d+ 文字以上\?$/)
|
|
expect(titleQuestion?.id).toMatch(/^title:length-at-least:\d+$/)
|
|
})
|
|
})
|
|
|
|
describe('Gekanator API writers', () => {
|
|
beforeEach(() => {
|
|
mockedApiPost.mockReset()
|
|
})
|
|
|
|
it('sends game results using snake_case request keys', async () => {
|
|
mockedApiPost.mockResolvedValue({ id: 100 })
|
|
|
|
const answers: GekanatorAnswerLog[] = [
|
|
{
|
|
questionId: 'tag:character:喜多郁代',
|
|
questionText: '喜多ちゃんが関係してる?',
|
|
questionCondition: {
|
|
type: 'tag',
|
|
key: 'character:喜多郁代',
|
|
},
|
|
answer: 'yes',
|
|
originalAnswer: 'partial',
|
|
},
|
|
]
|
|
|
|
await expect(
|
|
saveGekanatorGame({
|
|
guessedPostId: 1,
|
|
correctPostId: 2,
|
|
answers,
|
|
}),
|
|
).resolves.toEqual({ id: 100 })
|
|
|
|
expect(mockedApiPost).toHaveBeenCalledWith('/gekanator/games', {
|
|
guessed_post_id: 1,
|
|
correct_post_id: 2,
|
|
answers: [
|
|
{
|
|
question_id: 'tag:character:喜多郁代',
|
|
question_text: '喜多ちゃんが関係してる?',
|
|
question_condition: {
|
|
type: 'tag',
|
|
key: 'character:喜多郁代',
|
|
},
|
|
answer: 'yes',
|
|
original_answer: 'partial',
|
|
},
|
|
],
|
|
})
|
|
})
|
|
|
|
it('sends question suggestions using snake_case request keys', async () => {
|
|
mockedApiPost.mockResolvedValue({
|
|
id: 10,
|
|
count: 1,
|
|
})
|
|
|
|
await expect(
|
|
saveGekanatorQuestionSuggestion({
|
|
gekanatorGameId: 100,
|
|
questionText: '喜多ちゃんが泣いてる?',
|
|
answer: 'yes',
|
|
}),
|
|
).resolves.toEqual({
|
|
id: 10,
|
|
count: 1,
|
|
})
|
|
|
|
expect(mockedApiPost).toHaveBeenCalledWith('/gekanator/question_suggestions', {
|
|
gekanator_game_id: 100,
|
|
question_text: '喜多ちゃんが泣いてる?',
|
|
answer: 'yes',
|
|
})
|
|
})
|
|
|
|
it('sends extra question answers using snake_case request keys', async () => {
|
|
mockedApiPost.mockResolvedValue({
|
|
count: 2,
|
|
})
|
|
|
|
await saveGekanatorExtraQuestionAnswers({
|
|
gameId: 100,
|
|
answers: [
|
|
{
|
|
questionId: 10,
|
|
answer: 'yes',
|
|
},
|
|
{
|
|
questionId: 11,
|
|
answer: 'probably_no',
|
|
},
|
|
],
|
|
})
|
|
|
|
expect(mockedApiPost).toHaveBeenCalledWith(
|
|
'/gekanator/games/100/extra_question_answers',
|
|
{
|
|
answers: [
|
|
{
|
|
question_id: 10,
|
|
answer: 'yes',
|
|
},
|
|
{
|
|
question_id: 11,
|
|
answer: 'probably_no',
|
|
},
|
|
],
|
|
},
|
|
)
|
|
})
|
|
})
|