このコミットが含まれているのは:
@@ -206,6 +206,40 @@ RSpec.describe 'Gekanator learning API', type: :request do
|
|||||||
expect(example.gekanator_game_id).to eq(json['id'])
|
expect(example.gekanator_game_id).to eq(json['id'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'learns accepted post_similarity answers from main game logs' do
|
||||||
|
sign_in_as admin
|
||||||
|
|
||||||
|
question = create_post_similarity_question!(text: '泣いてる?')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
post '/gekanator/games', params: {
|
||||||
|
guessed_post_id: guessed_post.id,
|
||||||
|
correct_post_id: correct_post.id,
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
question_id: "post-similarity:#{question.id}",
|
||||||
|
question_text: '泣いてる?',
|
||||||
|
answer: 'partial',
|
||||||
|
original_answer: 'partial'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}.to change { GekanatorQuestionExample.count }.by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
expect(json['learned_example_count']).to eq(1)
|
||||||
|
|
||||||
|
example = GekanatorQuestionExample.last
|
||||||
|
expect(example).to have_attributes(
|
||||||
|
gekanator_question_id: question.id,
|
||||||
|
post_id: correct_post.id,
|
||||||
|
user_id: admin.id,
|
||||||
|
answer: 'partial',
|
||||||
|
source: 'post_game_answer'
|
||||||
|
)
|
||||||
|
expect(example.gekanator_game_id).to eq(json['id'])
|
||||||
|
end
|
||||||
|
|
||||||
it 'does not learn fact questions or nico tag questions from main game logs' do
|
it 'does not learn fact questions or nico tag questions from main game logs' do
|
||||||
sign_in_as admin
|
sign_in_as admin
|
||||||
|
|
||||||
@@ -550,6 +584,37 @@ RSpec.describe 'Gekanator learning API', type: :request do
|
|||||||
expect(json['questions'].map { _1['id'] }).to include(existing.id)
|
expect(json['questions'].map { _1['id'] }).to include(existing.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'prioritizes questions the current user has not answered' do
|
||||||
|
sign_in_as admin
|
||||||
|
|
||||||
|
answered = create_post_similarity_question!(
|
||||||
|
text: 'already answered?',
|
||||||
|
priority_weight: 3.0
|
||||||
|
)
|
||||||
|
GekanatorQuestionExample.create!(
|
||||||
|
gekanator_question: answered,
|
||||||
|
post: other_post,
|
||||||
|
user: admin,
|
||||||
|
answer: 'yes',
|
||||||
|
source: 'post_game_extra'
|
||||||
|
)
|
||||||
|
|
||||||
|
unanswered =
|
||||||
|
6.times.map { |index|
|
||||||
|
create_post_similarity_question!(
|
||||||
|
text: "unanswered #{index}?",
|
||||||
|
priority_weight: 0.5
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get "/gekanator/games/#{game.id}/extra_questions"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json['questions'].map { _1['id'] }).to match_array(
|
||||||
|
unanswered.map(&:id)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it 'can return questions already asked in the game using snake_case question_id' do
|
it 'can return questions already asked in the game using snake_case question_id' do
|
||||||
sign_in_as admin
|
sign_in_as admin
|
||||||
|
|
||||||
|
|||||||
@@ -400,6 +400,10 @@ describe('Gekanator API writers', () => {
|
|||||||
type: 'tag',
|
type: 'tag',
|
||||||
key: 'character:喜多郁代',
|
key: 'character:喜多郁代',
|
||||||
},
|
},
|
||||||
|
questionMode: 'normal',
|
||||||
|
questionPurpose: 'effective_user_suggested',
|
||||||
|
effectiveQuestion: true,
|
||||||
|
learningQuestion: false,
|
||||||
answer: 'yes',
|
answer: 'yes',
|
||||||
originalAnswer: 'partial',
|
originalAnswer: 'partial',
|
||||||
},
|
},
|
||||||
@@ -424,6 +428,10 @@ describe('Gekanator API writers', () => {
|
|||||||
type: 'tag',
|
type: 'tag',
|
||||||
key: 'character:喜多郁代',
|
key: 'character:喜多郁代',
|
||||||
},
|
},
|
||||||
|
question_mode: 'normal',
|
||||||
|
question_purpose: 'effective_user_suggested',
|
||||||
|
effective_question: true,
|
||||||
|
learning_question: false,
|
||||||
answer: 'yes',
|
answer: 'yes',
|
||||||
original_answer: 'partial',
|
original_answer: 'partial',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -636,6 +636,10 @@ export const saveGekanatorGame = async ({
|
|||||||
question_id: answer.questionId,
|
question_id: answer.questionId,
|
||||||
question_text: answer.questionText,
|
question_text: answer.questionText,
|
||||||
question_condition: answer.questionCondition ?? null,
|
question_condition: answer.questionCondition ?? null,
|
||||||
|
question_mode: answer.questionMode,
|
||||||
|
question_purpose: answer.questionPurpose,
|
||||||
|
effective_question: answer.effectiveQuestion,
|
||||||
|
learning_question: answer.learningQuestion,
|
||||||
answer: answer.answer,
|
answer: answer.answer,
|
||||||
original_answer: answer.originalAnswer })) })
|
original_answer: answer.originalAnswer })) })
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ const gekanatorBackdropSource = gekanatorPageSource.slice (
|
|||||||
gekanatorPageSource.indexOf ('const GekanatorBackdrop'),
|
gekanatorPageSource.indexOf ('const GekanatorBackdrop'),
|
||||||
gekanatorPageSource.indexOf ('const expectedAnswerFor'))
|
gekanatorPageSource.indexOf ('const expectedAnswerFor'))
|
||||||
|
|
||||||
|
const gekanatorChooseQuestionSource = gekanatorPageSource.slice (
|
||||||
|
gekanatorPageSource.indexOf ('const chooseQuestion'),
|
||||||
|
gekanatorPageSource.indexOf ('const winningRunPriorityFor'))
|
||||||
|
|
||||||
|
const gekanatorFallbackQuestionSource = gekanatorPageSource.slice (
|
||||||
|
gekanatorPageSource.indexOf ('const chooseFallbackQuestion'),
|
||||||
|
gekanatorPageSource.indexOf ('const shouldEnterGuessPhase'))
|
||||||
|
|
||||||
|
|
||||||
describe('GekanatorBackdrop regression structure', () => {
|
describe('GekanatorBackdrop regression structure', () => {
|
||||||
it('keeps displayedBackdropMode as the render-time source of truth', () => {
|
it('keeps displayedBackdropMode as the render-time source of truth', () => {
|
||||||
@@ -103,6 +111,30 @@ describe('GekanatorBackdrop regression structure', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('Gekanator question selection regression structure', () => {
|
||||||
|
it('prefers normal questions after user_suggested quota has been met', () => {
|
||||||
|
const normalFallbackIndex = gekanatorChooseQuestionSource.indexOf (
|
||||||
|
'else if (normalPool.length > 0)')
|
||||||
|
const effectiveFallbackIndex = gekanatorChooseQuestionSource.indexOf (
|
||||||
|
'else if (effectiveUserSuggestedPool.length > 0)')
|
||||||
|
|
||||||
|
expect(normalFallbackIndex).toBeGreaterThan(0)
|
||||||
|
expect(effectiveFallbackIndex).toBeGreaterThan(0)
|
||||||
|
expect(normalFallbackIndex).toBeLessThan(effectiveFallbackIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not let fallback questions bypass user_suggested purpose tracking', () => {
|
||||||
|
expect(gekanatorFallbackQuestionSource).toContain (
|
||||||
|
"question.source !== 'user_suggested'")
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not show a fixed extra-question count in the extra learning UI', () => {
|
||||||
|
expect(gekanatorPageSource).not.toContain ('追加で 2 問まで答えてください。')
|
||||||
|
expect(gekanatorPageSource).toContain ('追加で質問に答えてください。')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
describe('isQuestionHardFilteredAfterAnswers', () => {
|
describe('isQuestionHardFilteredAfterAnswers', () => {
|
||||||
it('blocks only contradictory or redundant month questions after a yes answer', () => {
|
it('blocks only contradictory or redundant month questions after a yes answer', () => {
|
||||||
const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 }
|
const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 }
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする