fix(tasks): preserve newer state authority on rollback

This commit is contained in:
Brennan Benson
2026-08-05 19:05:41 -07:00
parent bab25ce3ab
commit ad54797612
4 changed files with 42 additions and 12 deletions
@@ -2862,8 +2862,9 @@ function PRActionsPanel({
)
onMutated()
} catch (err) {
authority.revert()
applyStatePatch(previousState)
if (authority.revert()) {
applyStatePatch(previousState)
}
toast.error(
err instanceof Error
? err.message
@@ -4194,7 +4195,7 @@ function GHEditSection({
const prevState = localState
// Why: without registry authority a search-lagged Tasks refetch silently
// reverts this row to its pre-mutation state (STA-3343).
let authority: { revert: () => void } | null = null
let authority: { revert: () => boolean } | null = null
run('state', {
mutate: () =>
runIssueUpdate({
@@ -4220,10 +4221,11 @@ function GHEditSection({
patchProjectRowIfNeeded({ state: newState })
},
onRevert: () => {
authority?.revert()
onStateChange(prevState)
patchWorkItem(item.id, { state: prevState }, item.repoId, { sourceContext })
patchProjectRowIfNeeded({ state: prevState })
if (authority?.revert()) {
onStateChange(prevState)
patchWorkItem(item.id, { state: prevState }, item.repoId, { sourceContext })
patchProjectRowIfNeeded({ state: prevState })
}
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('github-tasks')
@@ -249,7 +249,7 @@ describe('GitHubItemDialog source host boundaries', () => {
// Issue close/reopen: assert on optimistic apply, revert on failure.
const editSection = sourceBetween(source, 'function GHEditSection', 'const closeAsDuplicate')
expect(editSection).toContain('assertTaskPageGitHubDialogStateAuthority({')
expect(editSection).toContain('authority?.revert()')
expect(editSection).toContain('if (authority?.revert())')
// PR close/reopen + merge: same protection for the shared Tasks list rows.
const actionsSection = sourceBetween(
@@ -258,7 +258,7 @@ describe('GitHubItemDialog source host boundaries', () => {
'function CommentReplyForm'
)
expect(actionsSection.match(/assertTaskPageGitHubDialogStateAuthority\(\{/g)).toHaveLength(2)
expect(actionsSection).toContain('authority.revert()')
expect(actionsSection).toContain('if (authority.revert())')
expect(actionsSection).toContain("state: 'merged'")
})
})
@@ -9,6 +9,7 @@ import {
reapplyPendingTaskPageGitHubMutationsToCache
} from './task-page-github-work-item-mutations'
import {
deleteLastConfirmedClientValue,
getLastConfirmedClientValue,
getOrCreateQuietRevalidateState,
resetTaskPageGitHubMutationRegistryForTests,
@@ -150,7 +151,7 @@ describe('dialog state authority (STA-3343)', () => {
itemId: 'issue:1',
state: 'closed'
})
fresh.revert()
expect(fresh.revert()).toBe(true)
expect(getLastConfirmedClientValue(null, 'repo-1', 'issue:1', 'state')).toBeUndefined()
setLastConfirmedClientValue(null, 'repo-1', 'issue:1', 'state', 'open')
@@ -159,7 +160,28 @@ describe('dialog state authority (STA-3343)', () => {
itemId: 'issue:1',
state: 'closed'
})
layered.revert()
expect(layered.revert()).toBe(true)
expect(getLastConfirmedClientValue(null, 'repo-1', 'issue:1', 'state')).toBe('open')
})
it('does not roll back authority released by search or superseded by a newer state', () => {
setTaskPageGitHubMutationQueryKey('q')
const released = assertTaskPageGitHubDialogStateAuthority({
repoId: 'repo-1',
itemId: 'issue:1',
state: 'closed'
})
deleteLastConfirmedClientValue(null, 'repo-1', 'issue:1', 'state')
expect(released.revert()).toBe(false)
expect(getLastConfirmedClientValue(null, 'repo-1', 'issue:1', 'state')).toBeUndefined()
const superseded = assertTaskPageGitHubDialogStateAuthority({
repoId: 'repo-1',
itemId: 'issue:1',
state: 'closed'
})
setLastConfirmedClientValue(null, 'repo-1', 'issue:1', 'state', 'merged')
expect(superseded.revert()).toBe(false)
expect(getLastConfirmedClientValue(null, 'repo-1', 'issue:1', 'state')).toBe('merged')
})
})
@@ -32,7 +32,7 @@ export function assertTaskPageGitHubDialogStateAuthority(args: {
itemId: string
state: GitHubWorkItem['state']
sourceContext?: TaskSourceContext | null
}): { revert: () => void } {
}): { revert: () => boolean } {
const sourceScope =
args.sourceContext?.provider === 'github' ? getTaskSourceCacheScope(args.sourceContext) : null
const previous = getLastConfirmedClientValue(sourceScope, args.repoId, args.itemId, 'state')
@@ -41,6 +41,11 @@ export function assertTaskPageGitHubDialogStateAuthority(args: {
notifyTaskPageGitHubMutationRegistry()
return {
revert: () => {
const current = getLastConfirmedClientValue(sourceScope, args.repoId, args.itemId, 'state')
// A matching search adopt or newer mutation owns the state now.
if (current !== args.state) {
return false
}
if (previous === undefined) {
deleteLastConfirmedClientValue(sourceScope, args.repoId, args.itemId, 'state')
} else {
@@ -48,6 +53,7 @@ export function assertTaskPageGitHubDialogStateAuthority(args: {
}
markStateFamilyDirty(args.repoId, args.itemId)
notifyTaskPageGitHubMutationRegistry()
return true
}
}
}