Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1654,6 +1654,59 @@ describe('PaperReviewView', () => {
)
})

it('does not record a defer receipt for a proposal left during the await', async () => {
let resolveDefer!: (proposal: Proposal) => void
mocks.deferProposal.mockImplementationOnce(
() => new Promise<Proposal>((resolve) => { resolveDefer = resolve }),
)
const wrapper = await mountView([
makeProposal({ id: 'aaa-1', summary: 'First proposal' }),
makeProposal({ id: 'bbb-1', summary: 'Second proposal' }),
])

await wrapper.find('[data-serial="#AAA-"]').trigger('click')
await flushPromises()
await wrapper.find('[data-testid="decision-defer"]').trigger('click')

// The queue stays interactive while the defer request is in flight.
await wrapper.find('[data-serial="#BBB-"]').trigger('click')
await flushPromises()
// Keep the current explicit selection while removing the route hash; this
// makes a stale receipt visible if the continuation re-anchors itself.
await wrapper.vm.$router.replace('/workspace/review')
await flushPromises()

resolveDefer(makeProposal({
id: 'aaa-1',
summary: 'First proposal',
deferredUntil: new Date(Date.now() + 60 * 60_000).toISOString(),
}))
await flushPromises()

expect(mocks.deferProposal).toHaveBeenCalledWith('aaa-1')
expect(wrapper.get('[data-testid="paper-review-main"]').text()).toContain('Second proposal')
expect(wrapper.find('[data-testid="paper-review-decision-receipt"]').exists()).toBe(false)
})

it('records a defer receipt when the current decision locus still matches', async () => {
let resolveDefer!: (proposal: Proposal) => void
mocks.deferProposal.mockImplementationOnce(
() => new Promise<Proposal>((resolve) => { resolveDefer = resolve }),
)
const wrapper = await mountView([makeProposal({ id: 'matching-defer' })])

await wrapper.find('[data-testid="decision-defer"]').trigger('click')
await Promise.resolve()
resolveDefer(makeProposal({
id: 'matching-defer',
deferredUntil: new Date(Date.now() + 60 * 60_000).toISOString(),
}))
await flushPromises()

expect(wrapper.get('[data-testid="paper-review-decision-receipt"]').attributes('data-decision'))
.toBe('deferred')
})

it('removes a snoozed proposal from the visible queue after defer resolves', async () => {
const deferred = makeProposal({
id: 'snooze-me',
Expand Down
10 changes: 9 additions & 1 deletion frontend/taskdeck-web/src/views/paper/PaperReviewView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2051,7 +2051,15 @@ async function onDefer() {
// deep-linked proposal whose re-defer failed would otherwise vanish (its prior deferredUntil is
// still in effect) with no retry path, despite the error toast.
if (deferred) {
recordDecisionReceipt(p.id, 'deferred')
// The queue remains interactive while the request is in flight. Only
// surface a receipt when the reviewer is still at the decision locus that
// started this defer; a late response must not pull them back to proposal A
// after they have selected proposal B. Keep deep-link cleanup separate:
// it is tied to the successful server result, not to the visible receipt.
const currentDecisionLocusId = explicitActiveId.value ?? activeProposal.value?.id
if (proposalIdsEqual(currentDecisionLocusId, p.id)) {
recordDecisionReceipt(p.id, 'deferred')
}
void clearProposalDeepLink(p.id)
}
}
Expand Down
Loading