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
140 changes: 138 additions & 2 deletions wework/src/features/workbench/WorkbenchProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,9 @@ function ArchiveRemoteRuntimeTaskProbe() {
>
archive remote task
</button>
<button type="button" onClick={() => void workbench.refreshWorkLists()}>
refresh work lists
</button>
</div>
)
}
Expand Down Expand Up @@ -2514,6 +2517,71 @@ describe('WorkbenchProvider runtime tasks', () => {
)
})

test('does not leave cloud work stuck syncing when a sync is superseded', async () => {
const runtimeWork = deferred<RuntimeWorkListResponse>()
const services = createWorkbenchServices({
cloudBackgroundApi: {
listTeams: vi.fn().mockResolvedValue([]),
listDevices: vi.fn().mockResolvedValue([]),
listRuntimeWork: vi.fn(() => runtimeWork.promise),
},
})

renderWorkbench(
<>
<CloudWorkStatusProbe />
<BootstrapProbe />
</>,
services
)

await waitFor(() =>
expect(screen.getByTestId('cloud-work-availability')).toHaveTextContent('syncing')
)

await userEvent.click(screen.getByRole('button', { name: 'Refresh devices' }))
await act(async () => {
runtimeWork.resolve({ projects: [], chats: [], totalTasks: 0 })
await runtimeWork.promise
})

await waitFor(() =>
expect(screen.getByTestId('cloud-work-availability')).not.toHaveTextContent('syncing')
)
})

test('does not leave cloud work stuck syncing when a task is archived mid-sync', async () => {
const runtimeWork = deferred<RuntimeWorkListResponse>()
const runtimeWorkApi = createRuntimeWorkApiMock()
const services = createWorkbenchServices({
runtimeWorkApi: runtimeWorkApi as WorkbenchServices['runtimeWorkApi'],
cloudBackgroundApi: {
listTeams: vi.fn().mockResolvedValue([]),
listDevices: vi.fn().mockResolvedValue([]),
listRuntimeWork: vi.fn(() => runtimeWork.promise),
},
})

renderWorkbench(
<>
<CloudWorkStatusProbe />
<ArchiveRemoteRuntimeTaskProbe />
</>,
services
)

await waitFor(() =>
expect(screen.getByTestId('cloud-work-availability')).toHaveTextContent('syncing')
)

await userEvent.click(screen.getByText('archive remote task'))
await waitFor(() => expect(runtimeWorkApi.archiveConversation).toHaveBeenCalledTimes(1))

await waitFor(() =>
expect(screen.getByTestId('cloud-work-availability')).not.toHaveTextContent('syncing')
)
})

test('restores cached remote task summaries when the device is offline at startup', async () => {
writeCachedRemoteRuntimeWork(1, {
projects: [
Expand Down Expand Up @@ -6476,7 +6544,7 @@ describe('WorkbenchProvider runtime tasks', () => {
await waitFor(() => expect(screen.getByTestId('archive-result')).toHaveTextContent('archived'))
})

test('does not restore an archived remote task from the previous cloud snapshot', async () => {
test('archives a remote task locally without triggering a cloud sync', async () => {
const remoteRuntimeWork: RuntimeWorkListResponse = {
projects: [
{
Expand Down Expand Up @@ -6540,15 +6608,83 @@ describe('WorkbenchProvider runtime tasks', () => {
await userEvent.click(screen.getByText('archive remote task'))

await waitFor(() => expect(runtimeWorkApi.archiveConversation).toHaveBeenCalledTimes(1))
await waitFor(() => expect(cloudListRuntimeWork).toHaveBeenCalledTimes(2))
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('')
expect(cloudListRuntimeWork).toHaveBeenCalledTimes(1)

await userEvent.click(screen.getByText('refresh work lists'))
await waitFor(() => expect(cloudListRuntimeWork).toHaveBeenCalledTimes(2))
postArchiveCloudWork.resolve({ projects: [], chats: [], totalTasks: 0 })
await waitFor(() =>
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('')
)
})

test('keeps an archived remote task hidden when the local list refresh fails', async () => {
const remoteRuntimeWork: RuntimeWorkListResponse = {
projects: [
{
project: { key: 'remote-project', name: 'Remote Wegent' },
deviceWorkspaces: [
{
deviceId: 'remote-device',
deviceName: '10.201.3.200',
deviceStatus: 'online',
available: true,
workspacePath: '/srv/Wegent',
workspaceSource: 'remote',
remoteHostId: 'remote-device',
tasks: [
{
taskId: 'remote-task',
workspacePath: '/srv/Wegent',
title: 'Remote task',
runtime: 'codex',
},
],
},
],
totalTasks: 1,
},
],
chats: [],
totalTasks: 1,
}
const runtimeWorkApi = createRuntimeWorkApiMock({
listRuntimeWork: vi.fn().mockRejectedValue(new Error('local list unavailable')),
})
const services = createWorkbenchServices({
runtimeWorkApi: runtimeWorkApi as WorkbenchServices['runtimeWorkApi'],
cloudBackgroundApi: {
listTeams: vi.fn().mockResolvedValue([]),
listDevices: vi.fn().mockResolvedValue([
createDevice({
id: 2,
device_id: 'remote-device',
name: '10.201.3.200',
status: 'online',
is_default: false,
device_type: 'remote',
}),
]),
listRuntimeWork: vi.fn().mockResolvedValue(remoteRuntimeWork),
},
})

renderWorkbench(<ArchiveRemoteRuntimeTaskProbe />, services)

await waitFor(() =>
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('Remote task')
)
runtimeWorkApi.listRuntimeWork.mockClear()
await userEvent.click(screen.getByText('archive remote task'))

await waitFor(() => expect(runtimeWorkApi.archiveConversation).toHaveBeenCalledTimes(1))
expect(runtimeWorkApi.listRuntimeWork).toHaveBeenCalledTimes(1)
await waitFor(() =>
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('')
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

test('renders streaming runtime task chunks when the socket connects after chat start', async () => {
let streamHandlers: ChatStreamHandlers = {}
const subscribe = vi.fn((handlers: ChatStreamHandlers) => {
Expand Down
2 changes: 1 addition & 1 deletion wework/src/features/workbench/WorkbenchProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ export function WorkbenchProvider({
remoteProjectSyncSignatureRef.current = signature
void executorClient.runtime
.syncRuntimeRemoteProjects({ deviceId: localRuntimeStateDeviceId, projects })
.then(refreshWorkLists)
.then(() => refreshWorkLists())
.catch(error => {
remoteProjectSyncSignatureRef.current = ''
console.warn('[Wework] Failed to sync remote projects into Codex global state', error)
Expand Down
142 changes: 77 additions & 65 deletions wework/src/features/workbench/useWorkbenchDataRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import type { CloudRuntimeState, CloudWorkCheckKey, WorkbenchState } from '@/typ
import {
EMPTY_CLOUD_RUNTIME_STATE,
EMPTY_RUNTIME_WORK,
abandonCloudRuntimeSync,
clearCloudRuntimeSync,
filterDisconnectedRemoteRuntimeWork,
finishCloudRuntimeSync,
mergeDeviceLists,
Expand All @@ -36,6 +38,7 @@ import {
runtimeWorkContainsTask,
} from './workbenchRuntimeHelpers'
import type { WorkbenchServices } from './workbenchServices'
import type { RefreshWorkLists } from './workbenchContextTypes'
import {
readCachedRemoteRuntimeWork,
reconcileCachedRemoteRuntimeWork,
Expand Down Expand Up @@ -92,14 +95,11 @@ function removeRuntimeTasksFromCloudState(
runtimeWork: removeRuntimeTasks(snapshot.runtimeWork, addresses),
}
: null
return {
return clearCloudRuntimeSync({
...state,
availability:
state.inFlightRevision == null ? state.availability : state.lastGood ? 'stale' : 'idle',
current: removeFromSnapshot(state.current),
lastGood: removeFromSnapshot(state.lastGood),
inFlightRevision: null,
}
})
}

function removeRuntimeProjectFromCloudState(
Expand Down Expand Up @@ -335,7 +335,6 @@ export function useWorkbenchDataRefresh({
try {
if (cloudRuntimeStateRef.current.inFlightRevision != null) {
if (options?.trigger !== 'manual-refresh' || !backgroundApi?.listDevices) return
const inFlightRevision = cloudRuntimeStateRef.current.inFlightRevision
const inFlightBackgroundApi = backgroundApi
const devicesResult = await timedWorkbenchBootstrapRequest(
'cloudDevices',
Expand All @@ -347,7 +346,6 @@ export function useWorkbenchDataRefresh({
!isCurrentRefresh() ||
options?.isCancelled?.() ||
devicesResult.status !== 'fulfilled' ||
cloudRuntimeStateRef.current.inFlightRevision !== inFlightRevision ||
cloudBackgroundApiRef.current !== inFlightBackgroundApi
) {
return
Expand Down Expand Up @@ -436,6 +434,9 @@ export function useWorkbenchDataRefresh({
cloudRuntimeStateRef.current.inFlightRevision !== revision ||
cloudBackgroundApiRef.current !== backgroundApi
) {
if (revision != null) {
updateCloudRuntimeState(abandonCloudRuntimeSync(cloudRuntimeStateRef.current, revision))
}
return
}

Expand Down Expand Up @@ -623,64 +624,75 @@ export function useWorkbenchDataRefresh({
user,
])

const refreshWorkLists = useCallback(async () => {
const [devicesResult, runtimeWorkResult] = await Promise.all([
executorClient.commands.listDevices().catch(error => {
const cachedDevices = readCachedDeviceList()
if (cachedDevices.length === 0) throw error
return cachedDevices
}),
executorClient.runtime.listRuntimeWork().catch(() => undefined),
])
const devices = resolveDeviceListWithCache(devicesResult)
const visibleDevices = resolveDeviceListWithCache(
selectVisibleDevices(devices, cloudRuntimeStateRef.current)
)
const filteredRuntimeWorkResult = runtimeWorkResult
? filterRemovedRuntimeProjects(runtimeWorkResult)
: undefined
if (filteredRuntimeWorkResult) {
localRuntimeWorkRef.current = filteredRuntimeWorkResult
}
const localRuntimeWork = filteredRuntimeWorkResult ?? state.runtimeWork ?? EMPTY_RUNTIME_WORK
if (filteredRuntimeWorkResult && !services.cloudBackgroundApi?.listRuntimeWork) {
releaseConfirmedArchivedRuntimeTasks(filteredRuntimeWorkResult)
}
const runtimeWork = filteredRuntimeWorkResult
? selectVisibleRuntimeWork(localRuntimeWork, cloudRuntimeStateRef.current, visibleDevices)
: hasCloudBackgroundApi
? localRuntimeWork
: filterDisconnectedRemoteRuntimeWork(localRuntimeWork)
debugRuntimeSidebarState('refresh-resolved', {
source: filteredRuntimeWorkResult ? 'executor' : 'current-state',
executorTaskIds: summarizeRuntimeWorkTaskIds(filteredRuntimeWorkResult ?? null),
visibleTaskIds: summarizeRuntimeWorkTaskIds(runtimeWork),
})
dispatch({
type: 'lists_refreshed',
projects: state.projects,
devices: visibleDevices,
runtimeWork,
standaloneDeviceId: getPreferredStandaloneDeviceId(visibleDevices, state.standaloneDeviceId),
})
void refreshCloudBackgroundData(devices, localRuntimeWork, {
projects: state.projects,
standaloneDeviceId: state.standaloneDeviceId,
trigger: 'manual-refresh',
}).catch(() => undefined)
}, [
dispatch,
executorClient,
filterRemovedRuntimeProjects,
refreshCloudBackgroundData,
hasCloudBackgroundApi,
releaseConfirmedArchivedRuntimeTasks,
selectVisibleRuntimeWork,
services.cloudBackgroundApi,
state.projects,
state.runtimeWork,
state.standaloneDeviceId,
])
const refreshWorkLists: RefreshWorkLists = useCallback(
async options => {
const [devicesResult, runtimeWorkResult] = await Promise.all([
executorClient.commands.listDevices().catch(error => {
const cachedDevices = readCachedDeviceList()
if (cachedDevices.length === 0) throw error
return cachedDevices
}),
executorClient.runtime.listRuntimeWork().catch(() => undefined),
])
const devices = resolveDeviceListWithCache(devicesResult)
const visibleDevices = resolveDeviceListWithCache(
selectVisibleDevices(devices, cloudRuntimeStateRef.current)
)
const filteredRuntimeWorkResult = runtimeWorkResult
? filterRemovedRuntimeProjects(runtimeWorkResult)
: undefined
if (filteredRuntimeWorkResult) {
localRuntimeWorkRef.current = filteredRuntimeWorkResult
}
const localRuntimeWork = filteredRuntimeWorkResult ?? state.runtimeWork ?? EMPTY_RUNTIME_WORK
if (filteredRuntimeWorkResult && !services.cloudBackgroundApi?.listRuntimeWork) {
releaseConfirmedArchivedRuntimeTasks(filteredRuntimeWorkResult)
}
const runtimeWork = filteredRuntimeWorkResult
? selectVisibleRuntimeWork(localRuntimeWork, cloudRuntimeStateRef.current, visibleDevices)
: removeRuntimeTasks(
hasCloudBackgroundApi
? localRuntimeWork
: filterDisconnectedRemoteRuntimeWork(localRuntimeWork),
archivedRuntimeTaskAddressesRef.current
)
debugRuntimeSidebarState('refresh-resolved', {
source: filteredRuntimeWorkResult ? 'executor' : 'current-state',
executorTaskIds: summarizeRuntimeWorkTaskIds(filteredRuntimeWorkResult ?? null),
visibleTaskIds: summarizeRuntimeWorkTaskIds(runtimeWork),
})
dispatch({
type: 'lists_refreshed',
projects: state.projects,
devices: visibleDevices,
runtimeWork,
standaloneDeviceId: getPreferredStandaloneDeviceId(
visibleDevices,
state.standaloneDeviceId
),
})
if (options?.syncCloud !== false) {
void refreshCloudBackgroundData(devices, localRuntimeWork, {
projects: state.projects,
standaloneDeviceId: state.standaloneDeviceId,
trigger: 'manual-refresh',
}).catch(() => undefined)
}
},
[
dispatch,
executorClient,
filterRemovedRuntimeProjects,
refreshCloudBackgroundData,
hasCloudBackgroundApi,
releaseConfirmedArchivedRuntimeTasks,
selectVisibleRuntimeWork,
services.cloudBackgroundApi,
state.projects,
state.runtimeWork,
state.standaloneDeviceId,
]
)

const loadDevicesForRefresh = useCallback(
async (options?: { useCacheFallback?: boolean }): Promise<DeviceInfo[]> => {
Expand Down
Loading
Loading