このコミットが含まれているのは:
@@ -0,0 +1,296 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { apiPost } from '@/lib/api'
|
||||
import {
|
||||
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,
|
||||
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,
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
新しい課題から参照
ユーザをブロックする