human_task_agent_spec.rb 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. require 'rails_helper'
  2. describe Agents::HumanTaskAgent do
  3. before do
  4. @checker = Agents::HumanTaskAgent.new(name: 'my human task agent')
  5. @checker.options = @checker.default_options
  6. @checker.user = users(:bob)
  7. @checker.save!
  8. @event = Event.new
  9. @event.agent = agents(:bob_rain_notifier_agent)
  10. @event.payload = { 'foo' => { 'bar' => { 'baz' => 'a2b' } },
  11. 'name' => 'Joe' }
  12. @event.id = 345
  13. expect(@checker).to be_valid
  14. end
  15. describe 'validations' do
  16. it "validates that trigger_on is 'schedule' or 'event'" do
  17. @checker.options['trigger_on'] = 'foo'
  18. expect(@checker).not_to be_valid
  19. end
  20. it "requires expected_receive_period_in_days when trigger_on is set to 'event'" do
  21. @checker.options['trigger_on'] = 'event'
  22. @checker.options['expected_receive_period_in_days'] = nil
  23. expect(@checker).not_to be_valid
  24. @checker.options['expected_receive_period_in_days'] = 2
  25. expect(@checker).to be_valid
  26. end
  27. it "requires a positive submission_period when trigger_on is set to 'schedule'" do
  28. @checker.options['trigger_on'] = 'schedule'
  29. @checker.options['submission_period'] = nil
  30. expect(@checker).not_to be_valid
  31. @checker.options['submission_period'] = 2
  32. expect(@checker).to be_valid
  33. end
  34. it 'requires a hit.title' do
  35. @checker.options['hit']['title'] = ''
  36. expect(@checker).not_to be_valid
  37. end
  38. it 'requires a hit.description' do
  39. @checker.options['hit']['description'] = ''
  40. expect(@checker).not_to be_valid
  41. end
  42. it 'requires hit.assignments' do
  43. @checker.options['hit']['assignments'] = ''
  44. expect(@checker).not_to be_valid
  45. @checker.options['hit']['assignments'] = 0
  46. expect(@checker).not_to be_valid
  47. @checker.options['hit']['assignments'] = 'moose'
  48. expect(@checker).not_to be_valid
  49. @checker.options['hit']['assignments'] = '2'
  50. expect(@checker).to be_valid
  51. end
  52. it 'requires hit.questions' do
  53. old_questions = @checker.options['hit']['questions']
  54. @checker.options['hit']['questions'] = nil
  55. expect(@checker).not_to be_valid
  56. @checker.options['hit']['questions'] = []
  57. expect(@checker).not_to be_valid
  58. @checker.options['hit']['questions'] = [old_questions[0]]
  59. expect(@checker).to be_valid
  60. end
  61. it 'requires that all questions have key, name, required, type, and question' do
  62. old_questions = @checker.options['hit']['questions']
  63. @checker.options['hit']['questions'].first['key'] = ''
  64. expect(@checker).not_to be_valid
  65. @checker.options['hit']['questions'] = old_questions
  66. @checker.options['hit']['questions'].first['name'] = ''
  67. expect(@checker).not_to be_valid
  68. @checker.options['hit']['questions'] = old_questions
  69. @checker.options['hit']['questions'].first['required'] = nil
  70. expect(@checker).not_to be_valid
  71. @checker.options['hit']['questions'] = old_questions
  72. @checker.options['hit']['questions'].first['type'] = ''
  73. expect(@checker).not_to be_valid
  74. @checker.options['hit']['questions'] = old_questions
  75. @checker.options['hit']['questions'].first['question'] = ''
  76. expect(@checker).not_to be_valid
  77. end
  78. it "requires that all questions of type 'selection' have a selections array with keys and text" do
  79. @checker.options['hit']['questions'][0]['selections'] = []
  80. expect(@checker).not_to be_valid
  81. @checker.options['hit']['questions'][0]['selections'] = [{}]
  82. expect(@checker).not_to be_valid
  83. @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => '', 'text' => '' }]
  84. expect(@checker).not_to be_valid
  85. @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => '', 'text' => 'hi' }]
  86. expect(@checker).not_to be_valid
  87. @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => 'hi', 'text' => '' }]
  88. expect(@checker).not_to be_valid
  89. @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => 'hi', 'text' => 'hi' }]
  90. expect(@checker).to be_valid
  91. @checker.options['hit']['questions'][0]['selections'] = [{ 'key' => 'hi', 'text' => 'hi' }, {}]
  92. expect(@checker).not_to be_valid
  93. end
  94. it "requires that 'poll_options' be present and populated when 'combination_mode' is set to 'poll'" do
  95. @checker.options['combination_mode'] = 'poll'
  96. expect(@checker).not_to be_valid
  97. @checker.options['poll_options'] = {}
  98. expect(@checker).not_to be_valid
  99. @checker.options['poll_options'] = { 'title' => 'Take a poll about jokes',
  100. 'instructions' => 'Rank these by how funny they are',
  101. 'assignments' => 3,
  102. 'row_template' => '{{joke}}' }
  103. expect(@checker).to be_valid
  104. @checker.options['poll_options'] = { 'instructions' => 'Rank these by how funny they are',
  105. 'assignments' => 3,
  106. 'row_template' => '{{joke}}' }
  107. expect(@checker).not_to be_valid
  108. @checker.options['poll_options'] = { 'title' => 'Take a poll about jokes',
  109. 'assignments' => 3,
  110. 'row_template' => '{{joke}}' }
  111. expect(@checker).not_to be_valid
  112. @checker.options['poll_options'] = { 'title' => 'Take a poll about jokes',
  113. 'instructions' => 'Rank these by how funny they are',
  114. 'row_template' => '{{joke}}' }
  115. expect(@checker).not_to be_valid
  116. @checker.options['poll_options'] = { 'title' => 'Take a poll about jokes',
  117. 'instructions' => 'Rank these by how funny they are',
  118. 'assignments' => 3 }
  119. expect(@checker).not_to be_valid
  120. end
  121. it "requires that all questions be of type 'selection' when 'combination_mode' is 'take_majority'" do
  122. @checker.options['combination_mode'] = 'take_majority'
  123. expect(@checker).not_to be_valid
  124. @checker.options['hit']['questions'][1]['type'] = 'selection'
  125. @checker.options['hit']['questions'][1]['selections'] = @checker.options['hit']['questions'][0]['selections']
  126. expect(@checker).to be_valid
  127. end
  128. it "accepts 'take_majority': 'true' for legacy support" do
  129. @checker.options['take_majority'] = 'true'
  130. expect(@checker).not_to be_valid
  131. @checker.options['hit']['questions'][1]['type'] = 'selection'
  132. @checker.options['hit']['questions'][1]['selections'] = @checker.options['hit']['questions'][0]['selections']
  133. expect(@checker).to be_valid
  134. end
  135. end
  136. describe "when 'trigger_on' is set to 'schedule'" do
  137. before do
  138. @checker.options['trigger_on'] = 'schedule'
  139. @checker.options['submission_period'] = '2'
  140. @checker.options.delete('expected_receive_period_in_days')
  141. end
  142. it 'should check for reviewable HITs frequently' do
  143. expect(@checker).to receive(:review_hits).twice
  144. expect(@checker).to receive(:create_basic_hit).once
  145. @checker.check
  146. @checker.check
  147. end
  148. it "should create HITs every 'submission_period' hours" do
  149. now = Time.now
  150. allow(Time).to receive(:now) { now }
  151. expect(@checker).to receive(:review_hits).exactly(3).times
  152. expect(@checker).to receive(:create_basic_hit).twice
  153. @checker.check
  154. now += 1 * 60 * 60
  155. @checker.check
  156. now += 1 * 60 * 60
  157. @checker.check
  158. end
  159. it 'should ignore events' do
  160. expect(@checker).not_to receive(:create_basic_hit).with(anything)
  161. @checker.receive([events(:bob_website_agent_event)])
  162. end
  163. end
  164. describe "when 'trigger_on' is set to 'event'" do
  165. it 'should not create HITs during check but should check for reviewable HITs' do
  166. @checker.options['submission_period'] = '2'
  167. now = Time.now
  168. allow(Time).to receive(:now) { now }
  169. expect(@checker).to receive(:review_hits).exactly(3).times
  170. expect(@checker).not_to receive(:create_basic_hit)
  171. @checker.check
  172. now += 1 * 60 * 60
  173. @checker.check
  174. now += 1 * 60 * 60
  175. @checker.check
  176. end
  177. it 'should create HITs based on events' do
  178. expect(@checker).to receive(:create_basic_hit).with(events(:bob_website_agent_event)).once
  179. @checker.receive([events(:bob_website_agent_event)])
  180. end
  181. end
  182. describe 'creating hits' do
  183. it 'can create HITs based on events, interpolating their values' do
  184. @checker.options['hit']['title'] = 'Hi {{name}}'
  185. @checker.options['hit']['description'] = 'Make something for {{name}}'
  186. @checker.options['hit']['questions'][0]['name'] = '{{name}} Question 1'
  187. question_form = nil
  188. hit_interface = double('hit_interface', id: 123, url: 'https://')
  189. allow(hit_interface).to receive(:question_form).with(instance_of(Agents::HumanTaskAgent::AgentQuestionForm)) { |agent_question_form_instance|
  190. question_form = agent_question_form_instance
  191. }
  192. allow(hit_interface).to receive(:max_assignments=).with(@checker.options['hit']['assignments'])
  193. allow(hit_interface).to receive(:description=).with('Make something for Joe')
  194. allow(hit_interface).to receive(:lifetime=)
  195. allow(hit_interface).to receive(:reward=).with(@checker.options['hit']['reward'])
  196. expect(RTurk::Hit).to receive(:create).with(title: 'Hi Joe').and_yield(hit_interface).and_return(hit_interface)
  197. @checker.send :create_basic_hit, @event
  198. xml = question_form.to_xml
  199. expect(xml).to include('<Title>Hi Joe</Title>')
  200. expect(xml).to include('<Text>Make something for Joe</Text>')
  201. expect(xml).to include('<DisplayName>Joe Question 1</DisplayName>')
  202. expect(@checker.memory['hits'][123]['event_id']).to eq(@event.id)
  203. end
  204. it 'works without an event too' do
  205. @checker.options['hit']['title'] = 'Hi {{name}}'
  206. hit_interface = double('hit_interface', id: 123, url: 'https://')
  207. allow(hit_interface).to receive(:question_form).with(instance_of(Agents::HumanTaskAgent::AgentQuestionForm))
  208. allow(hit_interface).to receive(:max_assignments=).with(@checker.options['hit']['assignments'])
  209. allow(hit_interface).to receive(:description=)
  210. allow(hit_interface).to receive(:lifetime=)
  211. allow(hit_interface).to receive(:reward=).with(@checker.options['hit']['reward'])
  212. expect(RTurk::Hit).to receive(:create).with(title: 'Hi').and_yield(hit_interface).and_return(hit_interface)
  213. @checker.send :create_basic_hit
  214. end
  215. end
  216. describe 'reviewing HITs' do
  217. class FakeHit
  218. def initialize(options = {})
  219. @options = options
  220. end
  221. def assignments
  222. @options[:assignments] || []
  223. end
  224. def max_assignments
  225. @options[:max_assignments] || 1
  226. end
  227. def dispose!
  228. @disposed = true
  229. end
  230. def disposed?
  231. @disposed
  232. end
  233. end
  234. class FakeAssignment
  235. attr_accessor :approved
  236. def initialize(options = {})
  237. @options = options
  238. end
  239. def answers
  240. @options[:answers] || {}
  241. end
  242. def status
  243. @options[:status] || ''
  244. end
  245. def approve!
  246. @approved = true
  247. end
  248. end
  249. it 'should work on multiple HITs' do
  250. event2 = Event.new
  251. event2.agent = agents(:bob_rain_notifier_agent)
  252. event2.payload = { 'foo2' => { 'bar2' => { 'baz2' => 'a2b2' } },
  253. 'name2' => 'Joe2' }
  254. event2.id = 3452
  255. # It knows about two HITs from two different events.
  256. @checker.memory['hits'] = {}
  257. @checker.memory['hits']['JH3132836336DHG'] = { 'event_id' => @event.id }
  258. @checker.memory['hits']['JH39AA63836DHG'] = { 'event_id' => event2.id }
  259. hit_ids = %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345]
  260. expect(RTurk::GetReviewableHITs).to receive(:create) { double(hit_ids:) } # It sees 3 HITs.
  261. # It looksup the two HITs that it owns. Neither are ready yet.
  262. expect(RTurk::Hit).to receive(:new).with('JH3132836336DHG') { FakeHit.new }
  263. expect(RTurk::Hit).to receive(:new).with('JH39AA63836DHG') { FakeHit.new }
  264. @checker.send :review_hits
  265. end
  266. it "shouldn't do anything if an assignment isn't ready" do
  267. @checker.memory['hits'] = { 'JH3132836336DHG' => { 'event_id' => @event.id } }
  268. expect(RTurk::GetReviewableHITs).to receive(:create) {
  269. double(hit_ids: %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345])
  270. }
  271. assignments = [
  272. FakeAssignment.new(status: 'Accepted', answers: {}),
  273. FakeAssignment.new(status: 'Submitted', answers: { 'sentiment' => 'happy', 'feedback' => 'Take 2' })
  274. ]
  275. hit = FakeHit.new(max_assignments: 2, assignments:)
  276. expect(RTurk::Hit).to receive(:new).with('JH3132836336DHG') { hit }
  277. # One of the assignments isn't set to "Submitted", so this should get skipped for now.
  278. expect_any_instance_of(FakeAssignment).not_to receive(:answers)
  279. @checker.send :review_hits
  280. expect(assignments.all? { |a| a.approved == true }).to be_falsey
  281. expect(@checker.memory['hits']).to eq({ 'JH3132836336DHG' => { 'event_id' => @event.id } })
  282. end
  283. it "shouldn't do anything if an assignment is missing" do
  284. @checker.memory['hits'] = { 'JH3132836336DHG' => { 'event_id' => @event.id } }
  285. expect(RTurk::GetReviewableHITs).to receive(:create) {
  286. double(hit_ids: %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345])
  287. }
  288. assignments = [
  289. FakeAssignment.new(status: 'Submitted', answers: { 'sentiment' => 'happy', 'feedback' => 'Take 2' })
  290. ]
  291. hit = FakeHit.new(max_assignments: 2, assignments:)
  292. expect(RTurk::Hit).to receive(:new).with('JH3132836336DHG') { hit }
  293. # One of the assignments hasn't shown up yet, so this should get skipped for now.
  294. expect_any_instance_of(FakeAssignment).not_to receive(:answers)
  295. @checker.send :review_hits
  296. expect(assignments.all? { |a| a.approved == true }).to be_falsey
  297. expect(@checker.memory['hits']).to eq({ 'JH3132836336DHG' => { 'event_id' => @event.id } })
  298. end
  299. context 'emitting events' do
  300. before do
  301. @checker.memory['hits'] = { 'JH3132836336DHG' => { 'event_id' => @event.id } }
  302. expect(RTurk::GetReviewableHITs).to receive(:create) {
  303. double(hit_ids: %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345])
  304. }
  305. @assignments = [
  306. FakeAssignment.new(status: 'Submitted', answers: { 'sentiment' => 'neutral', 'feedback' => '' }),
  307. FakeAssignment.new(status: 'Submitted', answers: { 'sentiment' => 'happy', 'feedback' => 'Take 2' })
  308. ]
  309. @hit = FakeHit.new(max_assignments: 2, assignments: @assignments)
  310. expect(@hit).not_to be_disposed
  311. expect(RTurk::Hit).to receive(:new).with('JH3132836336DHG') { @hit }
  312. end
  313. it 'should create events when all assignments are ready' do
  314. expect do
  315. @checker.send :review_hits
  316. end.to change { Event.count }.by(1)
  317. expect(@assignments.all? { |a| a.approved == true }).to be_truthy
  318. expect(@hit).to be_disposed
  319. expect(@checker.events.last.payload['answers']).to eq([
  320. { 'sentiment' => 'neutral', 'feedback' => '' },
  321. { 'sentiment' => 'happy', 'feedback' => 'Take 2' }
  322. ])
  323. expect(@checker.memory['hits']).to eq({})
  324. end
  325. it 'should emit separate answers when options[:separate_answers] is true' do
  326. @checker.options[:separate_answers] = true
  327. expect do
  328. @checker.send :review_hits
  329. end.to change { Event.count }.by(2)
  330. expect(@assignments.all? { |a| a.approved == true }).to be_truthy
  331. expect(@hit).to be_disposed
  332. event1, event2 = @checker.events.last(2)
  333. expect(event1.payload).not_to have_key('answers')
  334. expect(event2.payload).not_to have_key('answers')
  335. expect(event1.payload['answer']).to eq({ 'sentiment' => 'happy', 'feedback' => 'Take 2' })
  336. expect(event2.payload['answer']).to eq({ 'sentiment' => 'neutral', 'feedback' => '' })
  337. expect(@checker.memory['hits']).to eq({})
  338. end
  339. end
  340. describe 'taking majority votes' do
  341. before do
  342. @checker.options['combination_mode'] = 'take_majority'
  343. @checker.memory['hits'] = { 'JH3132836336DHG' => { 'event_id' => @event.id } }
  344. expect(RTurk::GetReviewableHITs).to receive(:create) {
  345. double(hit_ids: %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345])
  346. }
  347. end
  348. it 'should take the majority votes of all questions' do
  349. @checker.options['hit']['questions'][1] = {
  350. 'type' => 'selection',
  351. 'key' => 'age_range',
  352. 'name' => 'Age Range',
  353. 'required' => 'true',
  354. 'question' => 'Please select your age range:',
  355. 'selections' =>
  356. [
  357. { 'key' => '<50', 'text' => '50 years old or younger' },
  358. { 'key' => '>50', 'text' => 'Over 50 years old' }
  359. ]
  360. }
  361. assignments = [
  362. FakeAssignment.new(status: 'Submitted', answers: { 'sentiment' => 'sad', 'age_range' => '<50' }),
  363. FakeAssignment.new(status: 'Submitted', answers: { 'sentiment' => 'neutral', 'age_range' => '>50' }),
  364. FakeAssignment.new(status: 'Submitted', answers: { 'sentiment' => 'happy', 'age_range' => '>50' }),
  365. FakeAssignment.new(status: 'Submitted', answers: { 'sentiment' => 'happy', 'age_range' => '>50' })
  366. ]
  367. hit = FakeHit.new(max_assignments: 4, assignments:)
  368. expect(RTurk::Hit).to receive(:new).with('JH3132836336DHG') { hit }
  369. expect do
  370. @checker.send :review_hits
  371. end.to change { Event.count }.by(1)
  372. expect(assignments.all? { |a| a.approved == true }).to be_truthy
  373. expect(@checker.events.last.payload['answers']).to eq([
  374. { 'sentiment' => 'sad', 'age_range' => '<50' },
  375. { 'sentiment' => 'neutral', 'age_range' => '>50' },
  376. { 'sentiment' => 'happy', 'age_range' => '>50' },
  377. { 'sentiment' => 'happy', 'age_range' => '>50' }
  378. ])
  379. expect(@checker.events.last.payload['counts']).to eq({
  380. 'sentiment' => { 'happy' => 2, 'sad' => 1,
  381. 'neutral' => 1 }, 'age_range' => { '>50' => 3, '<50' => 1 }
  382. })
  383. expect(@checker.events.last.payload['majority_answer']).to eq({ 'sentiment' => 'happy', 'age_range' => '>50' })
  384. expect(@checker.events.last.payload).not_to have_key('average_answer')
  385. expect(@checker.memory['hits']).to eq({})
  386. end
  387. it 'should also provide an average answer when all questions are numeric' do
  388. # it should accept 'take_majority': 'true' as well for legacy support. Demonstrating that here.
  389. @checker.options.delete :combination_mode
  390. @checker.options['take_majority'] = 'true'
  391. @checker.options['hit']['questions'] = [
  392. {
  393. 'type' => 'selection',
  394. 'key' => 'rating',
  395. 'name' => 'Rating',
  396. 'required' => 'true',
  397. 'question' => 'Please select a rating:',
  398. 'selections' =>
  399. [
  400. { 'key' => '1', 'text' => 'One' },
  401. { 'key' => '2', 'text' => 'Two' },
  402. { 'key' => '3', 'text' => 'Three' },
  403. { 'key' => '4', 'text' => 'Four' },
  404. { 'key' => '5.1', 'text' => 'Five Point One' }
  405. ]
  406. }
  407. ]
  408. assignments = [
  409. FakeAssignment.new(status: 'Submitted', answers: { 'rating' => '1' }),
  410. FakeAssignment.new(status: 'Submitted', answers: { 'rating' => '3' }),
  411. FakeAssignment.new(status: 'Submitted', answers: { 'rating' => '5.1' }),
  412. FakeAssignment.new(status: 'Submitted', answers: { 'rating' => '2' }),
  413. FakeAssignment.new(status: 'Submitted', answers: { 'rating' => '2' })
  414. ]
  415. hit = FakeHit.new(max_assignments: 5, assignments:)
  416. expect(RTurk::Hit).to receive(:new).with('JH3132836336DHG') { hit }
  417. expect do
  418. @checker.send :review_hits
  419. end.to change { Event.count }.by(1)
  420. expect(assignments.all? { |a| a.approved == true }).to be_truthy
  421. expect(@checker.events.last.payload['answers']).to eq([
  422. { 'rating' => '1' },
  423. { 'rating' => '3' },
  424. { 'rating' => '5.1' },
  425. { 'rating' => '2' },
  426. { 'rating' => '2' }
  427. ])
  428. expect(@checker.events.last.payload['counts']).to eq({ 'rating' => { '1' => 1, '2' => 2, '3' => 1, '4' => 0,
  429. '5.1' => 1 } })
  430. expect(@checker.events.last.payload['majority_answer']).to eq({ 'rating' => '2' })
  431. expect(@checker.events.last.payload['average_answer']).to eq({ 'rating' => (1 + 2 + 2 + 3 + 5.1) / 5.0 })
  432. expect(@checker.memory['hits']).to eq({})
  433. end
  434. end
  435. describe 'creating and reviewing polls' do
  436. before do
  437. @checker.options['combination_mode'] = 'poll'
  438. @checker.options['poll_options'] = {
  439. 'title' => 'Hi!',
  440. 'instructions' => 'hello!',
  441. 'assignments' => 2,
  442. 'row_template' => 'This is {{sentiment}}'
  443. }
  444. @event.save!
  445. expect(RTurk::GetReviewableHITs).to receive(:create) {
  446. double(hit_ids: %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345])
  447. }
  448. end
  449. it 'creates a poll using the row_template, message, and correct number of assignments' do
  450. @checker.memory['hits'] = { 'JH3132836336DHG' => { 'event_id' => @event.id } }
  451. # Mock out the HIT's submitted assignments.
  452. assignments = [
  453. FakeAssignment.new(status: 'Submitted',
  454. answers: { 'sentiment' => 'sad',
  455. 'feedback' => 'This is my feedback 1' }),
  456. FakeAssignment.new(status: 'Submitted',
  457. answers: { 'sentiment' => 'neutral',
  458. 'feedback' => 'This is my feedback 2' }),
  459. FakeAssignment.new(status: 'Submitted',
  460. answers: { 'sentiment' => 'happy',
  461. 'feedback' => 'This is my feedback 3' }),
  462. FakeAssignment.new(status: 'Submitted',
  463. answers: { 'sentiment' => 'happy',
  464. 'feedback' => 'This is my feedback 4' })
  465. ]
  466. hit = FakeHit.new(max_assignments: 4, assignments:)
  467. expect(RTurk::Hit).to receive(:new).with('JH3132836336DHG') { hit }
  468. expect(@checker.memory['hits']['JH3132836336DHG']).to be_present
  469. # Setup mocks for HIT creation
  470. question_form = nil
  471. hit_interface = double('hit_interface', id: 'JH39AA63836DH12345', url: 'https://')
  472. allow(hit_interface).to receive(:question_form).with(instance_of(Agents::HumanTaskAgent::AgentQuestionForm)) { |agent_question_form_instance|
  473. question_form = agent_question_form_instance
  474. }
  475. allow(hit_interface).to receive(:max_assignments=).with(@checker.options['poll_options']['assignments'])
  476. allow(hit_interface).to receive(:description=).with(@checker.options['poll_options']['instructions'])
  477. allow(hit_interface).to receive(:lifetime=)
  478. allow(hit_interface).to receive(:reward=).with(@checker.options['hit']['reward'])
  479. expect(RTurk::Hit).to receive(:create).with(title: 'Hi!').and_yield(hit_interface).and_return(hit_interface)
  480. # And finally, the test.
  481. # it does not emit an event until all poll results are in
  482. expect do
  483. @checker.send :review_hits
  484. end.to change { Event.count }.by(0)
  485. # it approves the existing assignments
  486. expect(assignments.all? { |a| a.approved == true }).to be_truthy
  487. expect(hit).to be_disposed
  488. # it creates a new HIT for the poll
  489. xml = question_form.to_xml
  490. expect(xml).to include('<Text>This is happy</Text>')
  491. expect(xml).to include('<Text>This is neutral</Text>')
  492. expect(xml).to include('<Text>This is sad</Text>')
  493. @checker.save
  494. @checker.reload
  495. expect(@checker.memory['hits']['JH3132836336DHG']).not_to be_present
  496. expect(@checker.memory['hits']['JH39AA63836DH12345']).to be_present
  497. expect(@checker.memory['hits']['JH39AA63836DH12345']['event_id']).to eq(@event.id)
  498. expect(@checker.memory['hits']['JH39AA63836DH12345']['type']).to eq('poll')
  499. expect(@checker.memory['hits']['JH39AA63836DH12345']['original_hit']).to eq('JH3132836336DHG')
  500. expect(@checker.memory['hits']['JH39AA63836DH12345']['answers'].length).to eq(4)
  501. end
  502. it 'emits an event when all poll results are in, containing the data from the best answer, plus all others' do
  503. original_answers = [
  504. { 'sentiment' => 'sad', 'feedback' => 'This is my feedback 1' },
  505. { 'sentiment' => 'neutral', 'feedback' => 'This is my feedback 2' },
  506. { 'sentiment' => 'happy', 'feedback' => 'This is my feedback 3' },
  507. { 'sentiment' => 'happy', 'feedback' => 'This is my feedback 4' }
  508. ]
  509. @checker.memory['hits'] = {
  510. 'JH39AA63836DH12345' => {
  511. 'type' => 'poll',
  512. 'original_hit' => 'JH3132836336DHG',
  513. 'answers' => original_answers,
  514. 'event_id' => 345
  515. }
  516. }
  517. # Mock out the HIT's submitted assignments.
  518. assignments = [
  519. FakeAssignment.new(status: 'Submitted', answers: { '1' => '2', '2' => '5', '3' => '3', '4' => '2' }),
  520. FakeAssignment.new(status: 'Submitted', answers: { '1' => '3', '2' => '4', '3' => '1', '4' => '4' })
  521. ]
  522. hit = FakeHit.new(max_assignments: 2, assignments:)
  523. expect(RTurk::Hit).to receive(:new).with('JH39AA63836DH12345') { hit }
  524. expect(@checker.memory['hits']['JH39AA63836DH12345']).to be_present
  525. expect do
  526. @checker.send :review_hits
  527. end.to change { Event.count }.by(1)
  528. # It emits an event
  529. expect(@checker.events.last.payload['answers']).to eq(original_answers)
  530. expect(@checker.events.last.payload['poll']).to eq([{ '1' => '2', '2' => '5', '3' => '3', '4' => '2' },
  531. { '1' => '3', '2' => '4', '3' => '1', '4' => '4' }])
  532. expect(@checker.events.last.payload['best_answer']).to eq({ 'sentiment' => 'neutral',
  533. 'feedback' => 'This is my feedback 2' })
  534. # it approves the existing assignments
  535. expect(assignments.all? { |a| a.approved == true }).to be_truthy
  536. expect(hit).to be_disposed
  537. expect(@checker.memory['hits']).to be_empty
  538. end
  539. end
  540. end
  541. end