mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix(native-chat): reconcile merged session tab publications
This commit is contained in:
@@ -129,6 +129,33 @@ describe('startStructuredCodexLaunch', () => {
|
||||
expect(mocks.abandonIntent).toHaveBeenCalledWith(intent)
|
||||
})
|
||||
|
||||
it('does not park or toast when close races the final verification retry', async () => {
|
||||
const worktreeId = 'wt-close-final-verify'
|
||||
const intent = launchIntent(worktreeId, 'session-close-final-verify')
|
||||
let resolveFinalRefresh!: (snapshots: RuntimeMobileSessionTabsResult[]) => void
|
||||
mocks.createIntent.mockReturnValueOnce(intent)
|
||||
mocks.launch.mockResolvedValue(intent.sessionId)
|
||||
vi.mocked(refreshLocalStructuredSessionTabs)
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([])
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<RuntimeMobileSessionTabsResult[]>((resolve) => {
|
||||
resolveFinalRefresh = resolve
|
||||
})
|
||||
)
|
||||
|
||||
startStructuredCodexLaunch(worktreeId)
|
||||
await vi.waitFor(() => expect(refreshLocalStructuredSessionTabs).toHaveBeenCalledTimes(3))
|
||||
expect(cancelStructuredCodexLaunch(worktreeId, intent.sessionId)).toBe(true)
|
||||
resolveFinalRefresh([])
|
||||
await flushLaunchSettlement()
|
||||
|
||||
expect(mocks.launch).toHaveBeenCalledTimes(2)
|
||||
expect(toast.error).not.toHaveBeenCalled()
|
||||
expect(mocks.abandonIntent).toHaveBeenCalledWith(intent)
|
||||
})
|
||||
|
||||
it('opens the chat without an informational progress toast', async () => {
|
||||
const worktreeId = 'wt-open-quiet'
|
||||
const intent = launchIntent(worktreeId, 'session-1')
|
||||
|
||||
@@ -133,6 +133,9 @@ async function retrySameIntent(state: StructuredLaunchState, priorError: unknown
|
||||
try {
|
||||
return await verifyPublishedSession(state.intent)
|
||||
} catch {
|
||||
if (state.cancelled) {
|
||||
throw new StructuredAgentSessionLaunchCancelledError()
|
||||
}
|
||||
state.visibilityUnknown = true
|
||||
notifyStructuredLaunchListeners()
|
||||
throw error ?? priorError
|
||||
|
||||
@@ -169,6 +169,40 @@ describe('applyWebSessionTabsSnapshot', () => {
|
||||
expect(shouldApplyWebSessionTabsSnapshot(sameEpochOlder, ENV)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps base and headless-merge publications in one freshness lineage', () => {
|
||||
const renderer = makeSnapshot([], {
|
||||
publicationEpoch: 'renderer:epoch-1',
|
||||
snapshotVersion: 14,
|
||||
activeTabType: null
|
||||
})
|
||||
const merged = makeSnapshot([], {
|
||||
publicationEpoch: 'renderer:epoch-1:headless-merge:abc123',
|
||||
snapshotVersion: 8,
|
||||
activeTabType: null
|
||||
})
|
||||
const refreshedRenderer = makeSnapshot([], {
|
||||
publicationEpoch: 'renderer:epoch-1',
|
||||
snapshotVersion: 15,
|
||||
activeTabType: null
|
||||
})
|
||||
|
||||
expect(shouldApplyWebSessionTabsSnapshot(renderer, ENV)).toBe(true)
|
||||
expect(shouldApplyWebSessionTabsSnapshot(merged, ENV)).toBe(true)
|
||||
// listAll can return the renderer base after a host-side headless merge;
|
||||
// the newer base revision must still be allowed to refresh the mirror.
|
||||
expect(shouldApplyWebSessionTabsSnapshot(refreshedRenderer, ENV)).toBe(true)
|
||||
expect(
|
||||
shouldApplyWebSessionTabsSnapshot(
|
||||
makeSnapshot([], {
|
||||
publicationEpoch: 'renderer:epoch-1',
|
||||
snapshotVersion: 7,
|
||||
activeTabType: null
|
||||
}),
|
||||
ENV
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a delayed frame from an epoch superseded by a later restart', () => {
|
||||
const beforeRestart = makeSnapshot([], {
|
||||
publicationEpoch: 'epoch-before-restart',
|
||||
|
||||
@@ -125,6 +125,22 @@ export function isRetiredSessionTabsPublicationEpoch(
|
||||
return hasRetiredValue(sessionTabsPublicationEpochHistoryByWorktree.get(key), publicationEpoch)
|
||||
}
|
||||
|
||||
/**
|
||||
* A headless merge keeps the renderer publication as its base epoch while
|
||||
* adding runtime-owned surfaces. Treat both forms as one ordering lineage.
|
||||
*/
|
||||
export function sameSessionTabsPublicationLineage(left: string, right: string): boolean {
|
||||
return (
|
||||
left === right ||
|
||||
((left.includes(':headless-merge:') || right.includes(':headless-merge:')) &&
|
||||
left.split(':headless-merge:')[0] === right.split(':headless-merge:')[0])
|
||||
)
|
||||
}
|
||||
|
||||
export function isHeadlessMergeSessionTabsPublication(publicationEpoch: string): boolean {
|
||||
return publicationEpoch.includes(':headless-merge:')
|
||||
}
|
||||
|
||||
export function noteSessionTabsPublicationEpoch(
|
||||
key: string,
|
||||
publicationEpoch: string
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
import {
|
||||
acceptSessionTabsRuntimeId,
|
||||
isRetiredSessionTabsPublicationEpoch,
|
||||
noteSessionTabsPublicationEpoch
|
||||
isHeadlessMergeSessionTabsPublication,
|
||||
noteSessionTabsPublicationEpoch,
|
||||
sameSessionTabsPublicationLineage
|
||||
} from './publisher-identity-fences'
|
||||
import {
|
||||
sessionTabsFreshnessKey,
|
||||
@@ -78,7 +80,20 @@ export function decideWebSessionTabsSnapshot(
|
||||
return WEB_SESSION_TABS_FRAME_UNMIRRORED
|
||||
}
|
||||
const current = latestSessionTabsSnapshotByWorktree.get(key)
|
||||
if (isRetiredSessionTabsPublicationEpoch(key, snapshot.publicationEpoch)) {
|
||||
const currentSharesPublicationLineage = Boolean(
|
||||
current &&
|
||||
sameSessionTabsPublicationLineage(current.publicationEpoch, snapshot.publicationEpoch)
|
||||
)
|
||||
const currentIsHeadlessMerge = current
|
||||
? isHeadlessMergeSessionTabsPublication(current.publicationEpoch)
|
||||
: false
|
||||
const comparePublicationVersions =
|
||||
currentSharesPublicationLineage &&
|
||||
(currentIsHeadlessMerge || !isHeadlessMergeSessionTabsPublication(snapshot.publicationEpoch))
|
||||
if (
|
||||
isRetiredSessionTabsPublicationEpoch(key, snapshot.publicationEpoch) &&
|
||||
!currentSharesPublicationLineage
|
||||
) {
|
||||
return WEB_SESSION_TABS_FRAME_OUTRANKED
|
||||
}
|
||||
const replayable = replayableSessionTabsSnapshotByWorktree.get(key)
|
||||
@@ -93,7 +108,8 @@ export function decideWebSessionTabsSnapshot(
|
||||
// Why: reject stale snapshots only within an epoch; host restarts create a new epoch.
|
||||
if (
|
||||
current &&
|
||||
current.publicationEpoch === snapshot.publicationEpoch &&
|
||||
comparePublicationVersions &&
|
||||
sameSessionTabsPublicationLineage(current.publicationEpoch, snapshot.publicationEpoch) &&
|
||||
snapshot.snapshotVersion <= current.snapshotVersion &&
|
||||
!isExactCurrentReplay
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user