Reviewed-on: #365 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #365 でマージされました.
このコミットが含まれているのは:
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
candidatePostsFor,
|
||||
hardFilteredPostsForAnswer,
|
||||
recoverCandidatePosts,
|
||||
} from '@/lib/gekanatorCandidateRecovery'
|
||||
|
||||
import type {
|
||||
GekanatorAnswerLog,
|
||||
GekanatorAnswerValue,
|
||||
GekanatorQuestion,
|
||||
} from '@/lib/gekanator'
|
||||
import type { Post } from '@/types'
|
||||
|
||||
|
||||
const post = (id: number): Post => ({
|
||||
id,
|
||||
versionNo: 1,
|
||||
url: `https://example.com/posts/${ id }`,
|
||||
title: `post ${ id }`,
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
const postSimilarityQuestion = (
|
||||
id: string,
|
||||
answers: Record<`${ number }`, GekanatorAnswerValue>,
|
||||
): GekanatorQuestion => ({
|
||||
id,
|
||||
text: `${ id }?`,
|
||||
kind: 'post_similarity',
|
||||
condition: {
|
||||
type: 'post-similarity',
|
||||
postId: 9999,
|
||||
answer: 'yes',
|
||||
threshold: 0.65 },
|
||||
source: 'user_suggested',
|
||||
priorityWeight: 1,
|
||||
exampleAnswers: answers,
|
||||
test: candidate => answers[String (candidate.id) as `${ number }`] === 'yes',
|
||||
})
|
||||
|
||||
|
||||
const answer = (
|
||||
question: GekanatorQuestion,
|
||||
value: GekanatorAnswerValue,
|
||||
): GekanatorAnswerLog => ({
|
||||
questionId: question.id,
|
||||
questionText: question.text,
|
||||
questionCondition: question.condition,
|
||||
answer: value,
|
||||
originalAnswer: value,
|
||||
})
|
||||
|
||||
|
||||
describe('candidatePostsFor', () => {
|
||||
it('lets recovered candidates ignore old answers but not later answers', () => {
|
||||
const posts = [post (1), post (2), post (3)]
|
||||
const oldQuestion = postSimilarityQuestion ('old', {
|
||||
1: 'no',
|
||||
2: 'yes',
|
||||
3: 'yes',
|
||||
})
|
||||
const laterQuestion = postSimilarityQuestion ('later', {
|
||||
1: 'no',
|
||||
2: 'no',
|
||||
3: 'yes',
|
||||
})
|
||||
|
||||
const candidates = candidatePostsFor ({
|
||||
posts,
|
||||
questions: [oldQuestion, laterQuestion],
|
||||
answers: [answer (oldQuestion, 'yes'), answer (laterQuestion, 'yes')],
|
||||
softenedQuestionIds: new Set (),
|
||||
rejectedPostIds: new Set (),
|
||||
recoveredCandidatePosts: new Map ([
|
||||
[1, 1],
|
||||
[3, 1],
|
||||
]) })
|
||||
|
||||
expect(candidates.map (candidate => candidate.id)).toEqual ([3])
|
||||
})
|
||||
|
||||
it('does not let recovered candidates bypass explicit rejected posts', () => {
|
||||
const posts = [post (1), post (2)]
|
||||
const question = postSimilarityQuestion ('question', {
|
||||
1: 'yes',
|
||||
2: 'yes',
|
||||
})
|
||||
|
||||
const candidates = candidatePostsFor ({
|
||||
posts,
|
||||
questions: [question],
|
||||
answers: [answer (question, 'yes')],
|
||||
softenedQuestionIds: new Set (),
|
||||
rejectedPostIds: new Set ([1]),
|
||||
recoveredCandidatePosts: new Map ([[1, 1]]) })
|
||||
|
||||
expect(candidates.map (candidate => candidate.id)).toEqual ([2])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('hardFilteredPostsForAnswer', () => {
|
||||
it('returns zero candidates without falling back to the original pool', () => {
|
||||
const posts = [post (1), post (2)]
|
||||
const question = postSimilarityQuestion ('question', {
|
||||
1: 'yes',
|
||||
2: 'yes',
|
||||
})
|
||||
|
||||
expect(hardFilteredPostsForAnswer ({
|
||||
posts,
|
||||
question,
|
||||
answer: 'no',
|
||||
})).toEqual ([])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('recoverCandidatePosts', () => {
|
||||
it('recovers high-score non-rejected, non-eligible candidates in staged batches', () => {
|
||||
const posts = Array.from ({ length: 10 }, (_value, index) => post (index + 1))
|
||||
const scores = new Map (posts.map (candidate => [candidate.id, candidate.id]))
|
||||
|
||||
const recovered = recoverCandidatePosts ({
|
||||
posts,
|
||||
scores,
|
||||
rejectedPostIds: new Set ([10]),
|
||||
recoveredCandidatePosts: new Map ([[8, 1]]),
|
||||
eligiblePostIds: new Set ([9]),
|
||||
answerCountAtRecovery: 2,
|
||||
recoveryStepCount: 0,
|
||||
})
|
||||
|
||||
expect(recovered?.recoveryStepCount).toBe (1)
|
||||
expect([...(recovered?.recoveredCandidatePosts.keys () ?? [])])
|
||||
.toEqual ([8, 7, 6, 5, 4, 3, 2])
|
||||
expect(recovered?.recoveredCandidatePosts.get (7)).toBe (2)
|
||||
})
|
||||
})
|
||||
新しい課題から参照
ユーザをブロックする