| 133 |
140
|
|
end
|
| 134 |
141
|
|
end
|
| 135 |
142
|
|
|
|
143
|
+ |
describe "an attempt holding the issue" do
|
|
144
|
+ |
test "starts the issue with no board anywhere", %{repository: repository} do
|
|
145
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Picked up"})
|
|
146
|
+ |
{:ok, untouched} = Issues.create_issue(repository, %{title: "Nobody has touched this"})
|
|
147
|
+ |
|
|
148
|
+ |
attempt(repository, issue, state: "running")
|
|
149
|
+ |
|
|
150
|
+ |
assert Issues.progress(issue) == "in_progress"
|
|
151
|
+ |
assert Issues.progress(untouched) == "to_do"
|
|
152
|
+ |
end
|
|
153
|
+ |
|
|
154
|
+ |
test "an admitted attempt has started it too", %{repository: repository} do
|
|
155
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Admitted"})
|
|
156
|
+ |
attempt(repository, issue, state: "admitted")
|
|
157
|
+ |
|
|
158
|
+ |
assert Issues.progress(issue) == "in_progress"
|
|
159
|
+ |
end
|
|
160
|
+ |
|
|
161
|
+ |
test "a terminal attempt has released the claim", %{repository: repository} do
|
|
162
|
+ |
for state <- ~w(completed failed cancelled) do
|
|
163
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Attempt #{state}"})
|
|
164
|
+ |
attempt(repository, issue, state: state)
|
|
165
|
+ |
|
|
166
|
+ |
assert Issues.progress(issue) == "to_do", "expected #{state} to stop claiming"
|
|
167
|
+ |
end
|
|
168
|
+ |
end
|
|
169
|
+ |
|
|
170
|
+ |
test "an attempt past its deadline stops claiming work", %{repository: repository} do
|
|
171
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Ran over"})
|
|
172
|
+ |
|
|
173
|
+ |
attempt(repository, issue,
|
|
174
|
+ |
state: "running",
|
|
175
|
+ |
deadline_at: DateTime.add(DateTime.utc_now(), -60, :second)
|
|
176
|
+ |
)
|
|
177
|
+ |
|
|
178
|
+ |
assert Issues.progress(issue) == "to_do"
|
|
179
|
+ |
end
|
|
180
|
+ |
|
|
181
|
+ |
test "a dark attempt is withheld rather than published as progress", %{
|
|
182
|
+ |
repository: repository
|
|
183
|
+ |
} do
|
|
184
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Withheld"})
|
|
185
|
+ |
attempt(repository, issue, state: "running", transparency_tier: "dark")
|
|
186
|
+ |
|
|
187
|
+ |
assert Issues.progress(issue) == "to_do"
|
|
188
|
+ |
end
|
|
189
|
+ |
|
|
190
|
+ |
test "a revoked artifact link takes the attempt out of the projection", %{
|
|
191
|
+ |
repository: repository
|
|
192
|
+ |
} do
|
|
193
|
+ |
user = github_user("issue-progress-revoked")
|
|
194
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Revoked"})
|
|
195
|
+ |
|
|
196
|
+ |
link =
|
|
197
|
+ |
Repo.insert!(%ArtifactLink{
|
|
198
|
+ |
account_id: user.id,
|
|
199
|
+ |
artifact_type: "changelog",
|
|
200
|
+ |
artifact_ref: "sha",
|
|
201
|
+ |
repository_id: repository.id,
|
|
202
|
+ |
tier: "ledger",
|
|
203
|
+ |
revoked_at: DateTime.utc_now()
|
|
204
|
+ |
})
|
|
205
|
+ |
|
|
206
|
+ |
attempt(repository, issue, state: "running", artifact_link_id: link.id)
|
|
207
|
+ |
|
|
208
|
+ |
assert Issues.progress(issue) == "to_do"
|
|
209
|
+ |
end
|
|
210
|
+ |
|
|
211
|
+ |
test "the filter lists exactly what the derived value calls started", %{
|
|
212
|
+ |
repository: repository
|
|
213
|
+ |
} do
|
|
214
|
+ |
{:ok, started} = Issues.create_issue(repository, %{title: "Started"})
|
|
215
|
+ |
{:ok, _queued} = Issues.create_issue(repository, %{title: "Queued"})
|
|
216
|
+ |
attempt(repository, started, state: "running")
|
|
217
|
+ |
|
|
218
|
+ |
assert {[found], 1} = Issues.list_issues_page(repository, progress: "in_progress")
|
|
219
|
+ |
assert found.id == started.id
|
|
220
|
+ |
|
|
221
|
+ |
{queued, _total} = Issues.list_issues_page(repository, progress: "to_do")
|
|
222
|
+ |
assert Enum.map(queued, & &1.title) == ["Queued"]
|
|
223
|
+ |
end
|
|
224
|
+ |
end
|
|
225
|
+ |
|
|
226
|
+ |
describe "a session bound to the issue" do
|
|
227
|
+ |
test "an open thread starts the issue for the account running it", %{
|
|
228
|
+ |
repository: repository
|
|
229
|
+ |
} do
|
|
230
|
+ |
user = github_user("issue-progress-owner")
|
|
231
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Being worked"})
|
|
232
|
+ |
{:ok, untouched} = Issues.create_issue(repository, %{title: "Untouched"})
|
|
233
|
+ |
|
|
234
|
+ |
{:ok, _thread} = Threads.open(user, "Implement it", issue_id: issue.id)
|
|
235
|
+ |
|
|
236
|
+ |
assert Issues.progress(issue, user) == "in_progress"
|
|
237
|
+ |
assert Issues.progress(untouched, user) == "to_do"
|
|
238
|
+ |
end
|
|
239
|
+ |
|
|
240
|
+ |
test "a thread that has gone quiet stops claiming the issue", %{repository: repository} do
|
|
241
|
+ |
user = github_user("issue-progress-quiet")
|
|
242
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Walked away from"})
|
|
243
|
+ |
|
|
244
|
+ |
{:ok, thread} = Threads.open(user, "Implement it", issue_id: issue.id)
|
|
245
|
+ |
assert Issues.progress(issue, user) == "in_progress"
|
|
246
|
+ |
|
|
247
|
+ |
go_quiet(thread)
|
|
248
|
+ |
|
|
249
|
+ |
assert Issues.progress(issue, user) == "to_do"
|
|
250
|
+ |
assert Repo.get!(Thread, thread.id).status == "open"
|
|
251
|
+ |
end
|
|
252
|
+ |
|
|
253
|
+ |
test "a finished thread stops claiming the issue", %{repository: repository} do
|
|
254
|
+ |
user = github_user("issue-progress-finished")
|
|
255
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Session over"})
|
|
256
|
+ |
|
|
257
|
+ |
{:ok, thread} = Threads.open(user, "Implement it", issue_id: issue.id)
|
|
258
|
+ |
{:ok, _cancelled} = Threads.cancel(thread, "Done with it.")
|
|
259
|
+ |
|
|
260
|
+ |
assert Issues.progress(issue, user) == "to_do"
|
|
261
|
+ |
end
|
|
262
|
+ |
|
|
263
|
+ |
test "an owner-only thread never becomes another reader's fact", %{repository: repository} do
|
|
264
|
+ |
user = github_user("issue-progress-private")
|
|
265
|
+ |
stranger = github_user("issue-progress-stranger")
|
|
266
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Privately worked"})
|
|
267
|
+ |
|
|
268
|
+ |
{:ok, _dark} = Threads.open(user, "Implement it", issue_id: issue.id)
|
|
269
|
+ |
|
|
270
|
+ |
assert Issues.progress(issue, user) == "in_progress"
|
|
271
|
+ |
assert Issues.progress(issue, stranger) == "to_do"
|
|
272
|
+ |
assert Issues.progress(issue, nil) == "to_do"
|
|
273
|
+ |
end
|
|
274
|
+ |
|
|
275
|
+ |
test "a thread opened at a wider tier reaches every reader", %{repository: repository} do
|
|
276
|
+ |
user = github_user("issue-progress-wide")
|
|
277
|
+ |
stranger = github_user("issue-progress-wide-stranger")
|
|
278
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Openly worked"})
|
|
279
|
+ |
|
|
280
|
+ |
{:ok, _ledger} =
|
|
281
|
+ |
Threads.open(user, "Implement it", issue_id: issue.id, visibility: "ledger")
|
|
282
|
+ |
|
|
283
|
+ |
assert Issues.progress(issue, user) == "in_progress"
|
|
284
|
+ |
assert Issues.progress(issue, stranger) == "in_progress"
|
|
285
|
+ |
assert Issues.progress(issue, nil) == "in_progress"
|
|
286
|
+ |
end
|
|
287
|
+ |
|
|
288
|
+ |
test "the filter reads the same threads the derived value does", %{repository: repository} do
|
|
289
|
+ |
user = github_user("issue-progress-filter")
|
|
290
|
+ |
stranger = github_user("issue-progress-filter-stranger")
|
|
291
|
+ |
{:ok, started} = Issues.create_issue(repository, %{title: "Started"})
|
|
292
|
+ |
{:ok, _queued} = Issues.create_issue(repository, %{title: "Queued"})
|
|
293
|
+ |
|
|
294
|
+ |
{:ok, _thread} = Threads.open(user, "Implement it", issue_id: started.id)
|
|
295
|
+ |
|
|
296
|
+ |
assert {[found], 1} =
|
|
297
|
+ |
Issues.list_issues_page(repository, progress: "in_progress", reader: user)
|
|
298
|
+ |
|
|
299
|
+ |
assert found.id == started.id
|
|
300
|
+ |
|
|
301
|
+ |
assert {[], 0} =
|
|
302
|
+ |
Issues.list_issues_page(repository, progress: "in_progress", reader: stranger)
|
|
303
|
+ |
|
|
304
|
+ |
{queued, _total} =
|
|
305
|
+ |
Issues.list_issues_page(repository, progress: "to_do", reader: stranger)
|
|
306
|
+ |
|
|
307
|
+ |
assert Enum.map(queued, & &1.title) |> Enum.sort() == ["Queued", "Started"]
|
|
308
|
+ |
end
|
|
309
|
+ |
end
|
|
310
|
+ |
|
|
311
|
+ |
describe "binding a session to the issue its objective names" do
|
|
312
|
+ |
test "an objective naming an issue in the thread's repository binds it", %{
|
|
313
|
+ |
repository: repository
|
|
314
|
+ |
} do
|
|
315
|
+ |
user = github_user("issue-bind-owner")
|
|
316
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Named by the prompt"})
|
|
317
|
+ |
path = "#{repository.owner}/#{repository.name}"
|
|
318
|
+ |
|
|
319
|
+ |
{:ok, thread} =
|
|
320
|
+ |
Threads.open(user, "Implement issue ##{issue.number} and close it", repository: path)
|
|
321
|
+ |
|
|
322
|
+ |
assert thread.issue_id == issue.id
|
|
323
|
+ |
assert Issues.progress(issue, user) == "in_progress"
|
|
324
|
+ |
end
|
|
325
|
+ |
|
|
326
|
+ |
test "the qualified form binds when it names the same repository", %{
|
|
327
|
+ |
repository: repository
|
|
328
|
+ |
} do
|
|
329
|
+ |
user = github_user("issue-bind-qualified")
|
|
330
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Named in full"})
|
|
331
|
+ |
path = "#{repository.owner}/#{repository.name}"
|
|
332
|
+ |
|
|
333
|
+ |
{:ok, thread} = Threads.open(user, "Work #{path}##{issue.number}", repository: path)
|
|
334
|
+ |
|
|
335
|
+ |
assert thread.issue_id == issue.id
|
|
336
|
+ |
end
|
|
337
|
+ |
|
|
338
|
+ |
test "a reference to another repository binds nothing", %{repository: repository} do
|
|
339
|
+ |
user = github_user("issue-bind-cross")
|
|
340
|
+ |
other = repository_fixture()
|
|
341
|
+ |
{:ok, elsewhere} = Issues.create_issue(other, %{title: "Somewhere else"})
|
|
342
|
+ |
|
|
343
|
+ |
{:ok, thread} =
|
|
344
|
+ |
Threads.open(
|
|
345
|
+ |
user,
|
|
346
|
+ |
"Work #{other.owner}/#{other.name}##{elsewhere.number}",
|
|
347
|
+ |
repository: "#{repository.owner}/#{repository.name}"
|
|
348
|
+ |
)
|
|
349
|
+ |
|
|
350
|
+ |
assert is_nil(thread.issue_id)
|
|
351
|
+ |
end
|
|
352
|
+ |
|
|
353
|
+ |
test "a thread naming no repository binds nothing", %{repository: repository} do
|
|
354
|
+ |
user = github_user("issue-bind-unscoped")
|
|
355
|
+ |
{:ok, issue} = Issues.create_issue(repository, %{title: "Unreachable"})
|
|
356
|
+ |
|
|
357
|
+ |
{:ok, thread} = Threads.open(user, "Implement issue ##{issue.number}")
|
|
358
|
+ |
|
|
359
|
+ |
assert is_nil(thread.issue_id)
|
|
360
|
+ |
end
|
|
361
|
+ |
|
|
362
|
+ |
test "an issue the opener cannot read binds nothing" do
|
|
363
|
+ |
private = repository_fixture(%{visibility: "private"})
|
|
364
|
+ |
outsider = github_user("issue-bind-outsider")
|
|
365
|
+ |
{:ok, issue} = Issues.create_issue(private, %{title: "Private work"})
|
|
366
|
+ |
|
|
367
|
+ |
{:ok, thread} =
|
|
368
|
+ |
Threads.open(outsider, "Implement issue ##{issue.number}",
|
|
369
|
+ |
repository: "#{private.owner}/#{private.name}"
|
|
370
|
+ |
)
|
|
371
|
+ |
|
|
372
|
+ |
assert is_nil(thread.issue_id)
|
|
373
|
+ |
end
|
|
374
|
+ |
|
|
375
|
+ |
test "a number naming no issue binds nothing", %{repository: repository} do
|
|
376
|
+ |
user = github_user("issue-bind-missing")
|
|
377
|
+ |
|
|
378
|
+ |
{:ok, thread} =
|
|
379
|
+ |
Threads.open(user, "Implement issue #999999",
|
|
380
|
+ |
repository: "#{repository.owner}/#{repository.name}"
|
|
381
|
+ |
)
|
|
382
|
+ |
|
|
383
|
+ |
assert is_nil(thread.issue_id)
|
|
384
|
+ |
end
|
|
385
|
+ |
end
|
|
386
|
+ |
|
|
387
|
+ |
defp attempt(repository, issue, overrides) do
|
|
388
|
+ |
now = DateTime.utc_now()
|
|
389
|
+ |
|
|
390
|
+ |
defaults = [
|
|
391
|
+ |
repository_id: repository.id,
|
|
392
|
+ |
issue_id: issue.id,
|
|
393
|
+ |
target_kind: "computer",
|
|
394
|
+ |
requesting_principal: %{"kind" => "test"},
|
|
395
|
+ |
branch: "work/#{issue.number}-#{System.unique_integer([:positive])}",
|
|
396
|
+ |
state: "running",
|
|
397
|
+ |
transparency_tier: "ledger",
|
|
398
|
+ |
deadline_at: DateTime.add(now, 3600, :second),
|
|
399
|
+ |
admitted_at: now,
|
|
400
|
+ |
started_at: now
|
|
401
|
+ |
]
|
|
402
|
+ |
|
|
403
|
+ |
Repo.insert!(struct!(Assignment, Keyword.merge(defaults, overrides)))
|
|
404
|
+ |
end
|
|
405
|
+ |
|
|
406
|
+ |
# Silence, not a status change: the thread stays open and simply records
|
|
407
|
+ |
# nothing for longer than the quiet window.
|
|
408
|
+ |
defp go_quiet(%Thread{id: id}) do
|
|
409
|
+ |
quiet = DateTime.add(DateTime.utc_now(), -3 * 60 * 60, :second)
|
|
410
|
+ |
|
|
411
|
+ |
{1, _} =
|
|
412
|
+ |
Repo.update_all(from(thread in Thread, where: thread.id == ^id),
|
|
413
|
+ |
set: [updated_at: quiet]
|
|
414
|
+ |
)
|
|
415
|
+ |
end
|
|
416
|
+ |
|
| 136 |
417
|
|
defp place(board_repository, issue, column) do
|
| 137 |
418
|
|
{:ok, project} =
|
| 138 |
419
|
|
Projects.create_project(board_repository, %{title: "Board", owner: "OpenAgents"})
|