From 789e00b2e77553aa81de3346c15ab03cf22fa2bb Mon Sep 17 00:00:00 2001 From: miteruzo Date: Thu, 18 Jun 2026 01:04:50 +0900 Subject: [PATCH] #376 --- .../spec/requests/gekanator_learning_spec.rb | 65 +++++++++++++++++++ frontend/src/lib/gekanator.test.ts | 8 +++ frontend/src/lib/gekanator.ts | 4 ++ frontend/src/pages/GekanatorPage.test.tsx | 32 +++++++++ 4 files changed, 109 insertions(+) diff --git a/backend/spec/requests/gekanator_learning_spec.rb b/backend/spec/requests/gekanator_learning_spec.rb index bdea095..93928d3 100644 --- a/backend/spec/requests/gekanator_learning_spec.rb +++ b/backend/spec/requests/gekanator_learning_spec.rb @@ -206,6 +206,40 @@ RSpec.describe 'Gekanator learning API', type: :request do expect(example.gekanator_game_id).to eq(json['id']) 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 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) 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 sign_in_as admin diff --git a/frontend/src/lib/gekanator.test.ts b/frontend/src/lib/gekanator.test.ts index f152d98..dffdbd4 100644 --- a/frontend/src/lib/gekanator.test.ts +++ b/frontend/src/lib/gekanator.test.ts @@ -400,6 +400,10 @@ describe('Gekanator API writers', () => { type: 'tag', key: 'character:喜多郁代', }, + questionMode: 'normal', + questionPurpose: 'effective_user_suggested', + effectiveQuestion: true, + learningQuestion: false, answer: 'yes', originalAnswer: 'partial', }, @@ -424,6 +428,10 @@ describe('Gekanator API writers', () => { type: 'tag', key: 'character:喜多郁代', }, + question_mode: 'normal', + question_purpose: 'effective_user_suggested', + effective_question: true, + learning_question: false, answer: 'yes', original_answer: 'partial', }, diff --git a/frontend/src/lib/gekanator.ts b/frontend/src/lib/gekanator.ts index b5725d6..1299b0c 100644 --- a/frontend/src/lib/gekanator.ts +++ b/frontend/src/lib/gekanator.ts @@ -636,6 +636,10 @@ export const saveGekanatorGame = async ({ question_id: answer.questionId, question_text: answer.questionText, question_condition: answer.questionCondition ?? null, + question_mode: answer.questionMode, + question_purpose: answer.questionPurpose, + effective_question: answer.effectiveQuestion, + learning_question: answer.learningQuestion, answer: answer.answer, original_answer: answer.originalAnswer })) }) diff --git a/frontend/src/pages/GekanatorPage.test.tsx b/frontend/src/pages/GekanatorPage.test.tsx index ce98ff5..b42617f 100644 --- a/frontend/src/pages/GekanatorPage.test.tsx +++ b/frontend/src/pages/GekanatorPage.test.tsx @@ -60,6 +60,14 @@ const gekanatorBackdropSource = gekanatorPageSource.slice ( gekanatorPageSource.indexOf ('const GekanatorBackdrop'), 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', () => { 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', () => { it('blocks only contradictory or redundant month questions after a yes answer', () => { const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 }