グカネータ公開 (#361) (#368)

Reviewed-on: #368
Co-authored-by: miteruzo <miteruzo@naver.com>
Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #368 でマージされました.
このコミットが含まれているのは:
2026-06-14 05:33:39 +09:00
committed by みてるぞ
コミット 7ab46f907f
23個のファイルの変更3573行の追加466行の削除
+5 -4
ファイルの表示
@@ -52,16 +52,16 @@ RSpec.describe 'Gekanator games API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns not found without an admin user' do
it 'returns unauthorized without a user' do
post '/gekanator/games', params: {
guessed_post_id: guessed_post.id,
correct_post_id: guessed_post.id,
answers: [] }
expect(response).to have_http_status(:not_found)
expect(response).to have_http_status(:unauthorized)
end
it 'returns not found for a non-admin user' do
it 'stores a game for a non-admin user' do
sign_in_as user
post '/gekanator/games', params: {
@@ -69,7 +69,8 @@ RSpec.describe 'Gekanator games API', type: :request do
correct_post_id: guessed_post.id,
answers: [{ question_id: 'tag:1', answer: 'yes' }] }
expect(response).to have_http_status(:not_found)
expect(response).to have_http_status(:created)
expect(GekanatorGame.find(json['id']).user).to eq(user)
end
end
end
+182 -8
ファイルの表示
@@ -129,7 +129,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns not found for a non-admin user' do
it 'stores a game result for a non-admin user' do
sign_in_as member
post '/gekanator/games', params: {
@@ -138,7 +138,18 @@ RSpec.describe 'Gekanator learning API', type: :request do
answers: []
}
expect(response).to have_http_status(:not_found)
expect(response).to have_http_status(:created)
expect(GekanatorGame.find(json['id']).user).to eq(member)
end
it 'returns unauthorized without a user' do
post '/gekanator/games', params: {
guessed_post_id: guessed_post.id,
correct_post_id: correct_post.id,
answers: []
}
expect(response).to have_http_status(:unauthorized)
end
end
@@ -261,17 +272,57 @@ RSpec.describe 'Gekanator learning API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns not found for a non-admin user' do
it 'allows a non-admin user to suggest a question for their own game' do
member_game = GekanatorGame.create!(
user: member,
guessed_post: guessed_post,
correct_post: correct_post,
won: false,
question_count: 1,
answers: [{ 'question_id' => 'tag:1', 'answer' => 'yes' }]
)
sign_in_as member
post '/gekanator/question_suggestions', params: {
gekanator_game_id: game.id,
question_text: 'member question?',
answer: 'yes'
}
expect {
post '/gekanator/question_suggestions', params: {
gekanator_game_id: member_game.id,
question_text: 'member question?',
answer: 'yes'
}
}.to change { GekanatorQuestionSuggestion.count }.by(1)
expect(response).to have_http_status(:created)
expect(GekanatorQuestionSuggestion.last).to have_attributes(
gekanator_game_id: member_game.id,
user_id: member.id
)
end
it 'returns not found for another user game' do
sign_in_as member
expect {
post '/gekanator/question_suggestions', params: {
gekanator_game_id: game.id,
question_text: 'member question?',
answer: 'yes'
}
}.not_to change { GekanatorQuestionSuggestion.count }
expect(response).to have_http_status(:not_found)
end
it 'returns unauthorized without a user' do
expect {
post '/gekanator/question_suggestions', params: {
gekanator_game_id: game.id,
question_text: 'member question?',
answer: 'yes'
}
}.not_to change { GekanatorQuestionSuggestion.count }
expect(response).to have_http_status(:unauthorized)
end
end
describe 'GET /gekanator/games/:id/extra_questions' do
@@ -377,6 +428,38 @@ RSpec.describe 'Gekanator learning API', type: :request do
expect(response).to have_http_status(:ok)
expect(json['questions'].map { _1['id'] }).to contain_exactly(accepted.id)
end
it 'allows a non-admin user to fetch extra questions for their own game' do
member_game = GekanatorGame.create!(
user: member,
guessed_post: guessed_post,
correct_post: correct_post,
won: false,
question_count: 1,
answers: [{ 'question_id' => 'tag:1', 'answer' => 'yes' }]
)
accepted = create_post_similarity_question!(text: 'accepted?')
sign_in_as member
get "/gekanator/games/#{member_game.id}/extra_questions"
expect(response).to have_http_status(:ok)
expect(json['questions'].map { _1['id'] }).to include(accepted.id)
end
it 'returns not found for another user game' do
sign_in_as member
get "/gekanator/games/#{game.id}/extra_questions"
expect(response).to have_http_status(:not_found)
end
it 'returns unauthorized without a user' do
get "/gekanator/games/#{game.id}/extra_questions"
expect(response).to have_http_status(:unauthorized)
end
end
describe 'POST /gekanator/games/:id/extra_question_answers' do
@@ -503,6 +586,69 @@ RSpec.describe 'Gekanator learning API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
end
it 'allows a non-admin user to answer extra questions for their own game' do
member_game = GekanatorGame.create!(
user: member,
guessed_post: guessed_post,
correct_post: correct_post,
won: false,
question_count: 1,
answers: [{ 'question_id' => 'tag:1', 'answer' => 'yes' }]
)
question = create_post_similarity_question!(text: 'extra?')
sign_in_as member
expect {
post "/gekanator/games/#{member_game.id}/extra_question_answers", params: {
answers: [
{
question_id: question.id,
answer: 'yes'
}
]
}
}.to change { GekanatorQuestionExample.count }.by(1)
expect(response).to have_http_status(:created)
expect(GekanatorQuestionExample.last).to have_attributes(
user_id: member.id,
gekanator_game_id: member_game.id
)
end
it 'returns not found for another user game' do
question = create_post_similarity_question!(text: 'extra?')
sign_in_as member
expect {
post "/gekanator/games/#{game.id}/extra_question_answers", params: {
answers: [
{
question_id: question.id,
answer: 'yes'
}
]
}
}.not_to change { GekanatorQuestionExample.count }
expect(response).to have_http_status(:not_found)
end
it 'returns unauthorized without a user' do
question = create_post_similarity_question!(text: 'extra?')
post "/gekanator/games/#{game.id}/extra_question_answers", params: {
answers: [
{
question_id: question.id,
answer: 'yes'
}
]
}
expect(response).to have_http_status(:unauthorized)
end
end
describe 'GET /gekanator/questions' do
@@ -608,5 +754,33 @@ RSpec.describe 'Gekanator learning API', type: :request do
'length' => 21
)
end
it 'returns title-contains questions without authentication' do
GekanatorQuestion.create!(
text: '題名に「結束バンド」が含まれる?',
kind: 'title',
source: 'ai_generated',
status: 'accepted',
priority_weight: 0.95,
condition: {
type: 'title-contains',
text: '結束バンド'
},
created_by: admin
)
get '/gekanator/questions'
expect(response).to have_http_status(:ok)
question_json = json['questions'].find { _1['id'] == 'title:contains:結束バンド' }
expect(question_json).to include(
'text' => '題名に「結束バンド」が含まれる?',
'kind' => 'title'
)
expect(question_json['condition']).to include(
'type' => 'title-contains',
'text' => '結束バンド'
)
end
end
end
+112
ファイルの表示
@@ -0,0 +1,112 @@
require 'rails_helper'
RSpec.describe Gekanator::QuestionSuggestionAiConverter do
let(:user) { create(:user, :member) }
let(:guessed_post) { Post.create!(title: 'guess', url: 'https://example.com/guess') }
let(:correct_post) { Post.create!(title: 'correct', url: 'https://example.com/correct') }
let(:game) do
GekanatorGame.create!(
user: user,
guessed_post: guessed_post,
correct_post: correct_post,
won: false,
question_count: 1,
answers: [{ 'question_id' => 'tag:1', 'answer' => 'yes' }]
)
end
def create_suggestion!(question_text:, answer: 'yes')
GekanatorQuestionSuggestion.create!(
gekanator_game: game,
user: user,
question_text: question_text,
answer: answer
)
end
it 'converts title-contains suggestions to pending ai-generated questions' do
suggestion = create_suggestion!(question_text: '題名に「結束バンド」が含まれる?')
expect {
described_class.call(suggestion: suggestion, user: user)
}.to change { GekanatorQuestion.count }.by(1)
.and change { GekanatorAiRun.count }.by(1)
question = GekanatorQuestion.last
expect(question).to have_attributes(
text: '題名に「結束バンド」が含まれる?',
kind: 'title',
source: 'ai_generated',
status: 'pending',
priority_weight: 0.95,
gekanator_question_suggestion_id: suggestion.id,
created_by_id: user.id
)
expect(question.condition).to include(
'type' => 'title-contains',
'text' => '結束バンド'
)
expect(GekanatorAiRun.last).to have_attributes(
gekanator_question_suggestion_id: suggestion.id,
model: 'heuristic_converter_v1',
status: 'succeeded'
)
end
it 'converts concrete non-unknown suggestions to post-similarity questions' do
suggestion = create_suggestion!(
question_text: '喜多ちゃんが泣いてる?',
answer: 'partial'
)
question = described_class.call(suggestion: suggestion, user: user)
expect(question).to have_attributes(
text: '喜多ちゃんが泣いてる?',
kind: 'post_similarity',
source: 'ai_generated',
status: 'pending',
priority_weight: 1.0
)
expect(question.condition).to include(
'type' => 'post-similarity',
'postId' => correct_post.id,
'answer' => 'partial',
'threshold' => 0.65
)
end
it 'records a failed run when the suggestion cannot be converted' do
suggestion = create_suggestion!(
question_text: 'よく分からない質問?',
answer: 'unknown'
)
expect {
expect(described_class.call(suggestion: suggestion, user: user)).to be_nil
}.not_to change { GekanatorQuestion.count }
expect(GekanatorAiRun.last).to have_attributes(
gekanator_question_suggestion_id: suggestion.id,
status: 'failed'
)
end
it 'returns an existing generated question without creating a duplicate run' do
suggestion = create_suggestion!(question_text: 'タイトルは 10 文字以上?')
existing = GekanatorQuestion.create!(
text: 'タイトルは 10 文字以上?',
kind: 'title',
source: 'ai_generated',
status: 'pending',
priority_weight: 0.95,
condition: { type: 'title-length-at-least', length: 10 },
gekanator_question_suggestion: suggestion,
created_by: user
)
expect {
expect(described_class.call(suggestion: suggestion, user: user)).to eq(existing)
}.not_to change { GekanatorAiRun.count }
end
end