fix(agent-packages): show local inventory before refresh
This commit is contained in:
+11
-1
@@ -1805,6 +1805,7 @@ describe('registerIpcHandlers SSH hosts', () => {
|
||||
} as const
|
||||
const agentPackageManager = {
|
||||
getAllSnapshot: vi.fn(async () => agentPackageInventory),
|
||||
getAllInventory: vi.fn(async () => agentPackageInventory),
|
||||
forPlatform: vi.fn(() => agentPackageManager),
|
||||
download: vi.fn(
|
||||
async (
|
||||
@@ -1944,7 +1945,16 @@ describe('registerIpcHandlers SSH hosts', () => {
|
||||
).resolves.toEqual(agentPackageInventory)
|
||||
expect(
|
||||
agentPackageManager.getAllSnapshot
|
||||
).toHaveBeenCalledWith({ refresh: true })
|
||||
).toHaveBeenCalledWith({ refresh: true, localOnly: false })
|
||||
await expect(
|
||||
electronMocks.handlers.get(
|
||||
ipcChannels.sshHostsAgentPackageInventory
|
||||
)?.(event, { localOnly: true })
|
||||
).resolves.toEqual(agentPackageInventory)
|
||||
expect(agentPackageManager.getAllInventory).toHaveBeenCalledWith({
|
||||
refresh: false,
|
||||
localOnly: true
|
||||
})
|
||||
await expect(
|
||||
electronMocks.handlers.get(
|
||||
ipcChannels.sshHostsAgentPackageDownload
|
||||
|
||||
+3
-1
@@ -4855,7 +4855,9 @@ export function registerIpcHandlers(
|
||||
const request =
|
||||
agentPackageInventoryRequestSchema.parse(input ?? {})
|
||||
return agentPackageInventorySchema.parse(
|
||||
await agentPackageManager.getAllSnapshot(request)
|
||||
request.localOnly
|
||||
? await agentPackageManager.getAllInventory(request)
|
||||
: await agentPackageManager.getAllSnapshot(request)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -300,6 +300,17 @@ export class AgentPackageManager {
|
||||
})
|
||||
}
|
||||
|
||||
async getAllInventory(options: { refresh?: boolean } = {}): Promise<AgentPackageInventory> {
|
||||
const linux = await this.forPlatform('linux').getInventory(options)
|
||||
const darwin = this.forPlatform('darwin')
|
||||
darwin.#catalogState = this.forPlatform('linux').#catalogState
|
||||
const mac = await darwin.getInventory(options)
|
||||
return agentPackageInventorySchema.parse({
|
||||
...linux,
|
||||
entries: [...linux.entries, ...mac.entries]
|
||||
})
|
||||
}
|
||||
|
||||
#select(catalog: AgentPackageCatalog, architecture: AgentArchitecture, desktopVersion: string) {
|
||||
return selectLatestCompatibleEntry(catalog, architecture, desktopVersion, this.#platform)
|
||||
}
|
||||
|
||||
@@ -528,10 +528,10 @@ const desktopApi: DesktopApi = {
|
||||
handler
|
||||
)
|
||||
},
|
||||
getAgentPackageInventory: (refresh = false) =>
|
||||
getAgentPackageInventory: (refresh = false, localOnly = false) =>
|
||||
ipcRenderer.invoke(
|
||||
ipcChannels.sshHostsAgentPackageInventory,
|
||||
{ refresh }
|
||||
{ refresh, localOnly }
|
||||
) as Promise<AgentPackageInventory>,
|
||||
downloadAgentPackage: (architecture: AgentArchitecture, platform: 'linux' | 'darwin' = 'linux') =>
|
||||
ipcRenderer.invoke(
|
||||
|
||||
@@ -292,7 +292,7 @@ describe('sandboxed preload', () => {
|
||||
'ipcChannels.sshHostsAgentConnectionStatus'
|
||||
)
|
||||
expect(source).toContain(
|
||||
'getAgentPackageInventory: (refresh = false)'
|
||||
'getAgentPackageInventory: (refresh = false, localOnly = false)'
|
||||
)
|
||||
expect(source).toContain('downloadAgentPackage:')
|
||||
expect(source).toContain('importAgentPackage:')
|
||||
|
||||
@@ -42,7 +42,7 @@ const agentPackageInventory: AgentPackageInventory = {
|
||||
}
|
||||
]
|
||||
}
|
||||
const getAgentPackageInventory = vi.fn<(refresh?: boolean) => Promise<AgentPackageInventory>>()
|
||||
const getAgentPackageInventory = vi.fn<(refresh?: boolean, localOnly?: boolean) => Promise<AgentPackageInventory>>()
|
||||
const downloadAgentPackage = vi.fn<(architecture: string, platform: string) => Promise<AgentPackageInventory>>()
|
||||
const importAgentPackage = vi.fn<() => Promise<AgentPackageInventory | undefined>>()
|
||||
const exportAgentPackage = vi.fn<(architecture: string, platform: string) => Promise<void>>()
|
||||
@@ -84,11 +84,12 @@ describe('AgentPackageInventoryPanel', () => {
|
||||
expect(await screen.findByText('当前 1 / 2 个平台与架构目标可用。')).toBeInTheDocument()
|
||||
expect(screen.getByText('有更新')).toBeInTheDocument()
|
||||
expect(screen.getByText('未下载')).toBeInTheDocument()
|
||||
expect(getAgentPackageInventory).toHaveBeenCalledExactlyOnceWith(false)
|
||||
expect(getAgentPackageInventory).toHaveBeenNthCalledWith(1, false, true)
|
||||
expect(getAgentPackageInventory).toHaveBeenNthCalledWith(2, false)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '刷新包清单' }))
|
||||
await waitFor(() => expect(getAgentPackageInventory).toHaveBeenCalledTimes(2))
|
||||
expect(getAgentPackageInventory).toHaveBeenNthCalledWith(2, true)
|
||||
await waitFor(() => expect(getAgentPackageInventory).toHaveBeenCalledTimes(3))
|
||||
expect(getAgentPackageInventory).toHaveBeenNthCalledWith(3, true)
|
||||
|
||||
const x64Card = screen.getByText('Linux x64').closest('[role="listitem"]') as HTMLElement
|
||||
const arm64Card = screen.getByText('Linux arm64').closest('[role="listitem"]') as HTMLElement
|
||||
@@ -115,6 +116,37 @@ describe('AgentPackageInventoryPanel', () => {
|
||||
await waitFor(() => expect(onNotify).toHaveBeenCalledWith(expect.objectContaining({ dedupeKey: 'agent-package-imported' })))
|
||||
})
|
||||
|
||||
it('shows the local inventory before the online catalog returns', async () => {
|
||||
const localInventory: AgentPackageInventory = {
|
||||
...agentPackageInventory,
|
||||
catalog: { state: 'not-checked', checkedAt: null, error: null },
|
||||
entries: agentPackageInventory.entries.map((entry) => ({
|
||||
...entry,
|
||||
latestVersion: null,
|
||||
updateAvailable: false
|
||||
}))
|
||||
}
|
||||
let resolveOnline: ((inventory: AgentPackageInventory) => void) | undefined
|
||||
getAgentPackageInventory
|
||||
.mockResolvedValueOnce(localInventory)
|
||||
.mockReturnValueOnce(
|
||||
new Promise<AgentPackageInventory>((resolve) => {
|
||||
resolveOnline = resolve
|
||||
})
|
||||
)
|
||||
|
||||
render(<AgentPackageInventoryPanel />)
|
||||
expect(await screen.findByText('当前 1 / 2 个平台与架构目标可用。')).toBeInTheDocument()
|
||||
expect(getAgentPackageInventory).toHaveBeenNthCalledWith(1, false, true)
|
||||
expect(getAgentPackageInventory).toHaveBeenNthCalledWith(2, false)
|
||||
|
||||
await act(async () => {
|
||||
resolveOnline?.(agentPackageInventory)
|
||||
await Promise.resolve()
|
||||
})
|
||||
expect(await screen.findByText('有更新')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows loading and recovers from a refresh error', async () => {
|
||||
let resolveInventory: ((inventory: AgentPackageInventory) => void) | undefined
|
||||
getAgentPackageInventory.mockReturnValueOnce(
|
||||
@@ -133,15 +165,18 @@ describe('AgentPackageInventoryPanel', () => {
|
||||
await screen.findByText('inventory unavailable')
|
||||
fireEvent.click(screen.getByRole('button', { name: '刷新包清单' }))
|
||||
await waitFor(() => expect(screen.queryByText('inventory unavailable')).not.toBeInTheDocument())
|
||||
expect(getAgentPackageInventory).toHaveBeenCalledTimes(3)
|
||||
expect(getAgentPackageInventory).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('keeps local Agent packages usable when the online catalog is unavailable', async () => {
|
||||
getAgentPackageInventory.mockResolvedValueOnce({
|
||||
const unavailableInventory: AgentPackageInventory = {
|
||||
...agentPackageInventory,
|
||||
catalog: { state: 'unavailable', checkedAt: '2026-08-24T00:00:00.000Z', error: 'network unavailable' },
|
||||
entries: agentPackageInventory.entries.map((entry) => ({ ...entry, latestVersion: null, updateAvailable: false }))
|
||||
})
|
||||
}
|
||||
getAgentPackageInventory
|
||||
.mockResolvedValueOnce(unavailableInventory)
|
||||
.mockResolvedValueOnce(unavailableInventory)
|
||||
render(<AgentPackageInventoryPanel />)
|
||||
expect(await screen.findByText(/network unavailable/u)).toBeInTheDocument()
|
||||
const x64Card = screen.getByText('Linux x64').closest('[role="listitem"]') as HTMLElement
|
||||
|
||||
@@ -54,6 +54,9 @@ export function AgentPackageInventoryPanel({
|
||||
setAgentInventoryLoading(true)
|
||||
setAgentInventoryError(undefined)
|
||||
try {
|
||||
if (!refresh) {
|
||||
setAgentInventory(await getInventory(false, true))
|
||||
}
|
||||
setAgentInventory(await getInventory(refresh))
|
||||
} catch (reason) {
|
||||
setAgentInventoryError(
|
||||
@@ -259,16 +262,17 @@ export function AgentPackageInventoryPanel({
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{agentInventoryError && (
|
||||
<p className="settings-warning" role="alert">
|
||||
{agentInventoryError}
|
||||
</p>
|
||||
)}
|
||||
{agentInventoryLoading && !agentInventory ? (
|
||||
<p className="settings-notice" role="status">
|
||||
{t(
|
||||
'platformFeatures.remoteProjects.agentInventory.loading'
|
||||
)}
|
||||
</p>
|
||||
) : agentInventoryError ? (
|
||||
<p className="settings-warning" role="alert">
|
||||
{agentInventoryError}
|
||||
</p>
|
||||
) : agentInventory ? (
|
||||
<div
|
||||
aria-label={t(
|
||||
|
||||
@@ -135,9 +135,10 @@ describe('Agent package contracts', () => {
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('accepts only a bounded refresh flag for inventory reads', () => {
|
||||
it('accepts only bounded inventory read flags', () => {
|
||||
expect(agentPackageInventoryRequestSchema.parse({})).toEqual({
|
||||
refresh: false
|
||||
refresh: false,
|
||||
localOnly: false
|
||||
})
|
||||
expect(() =>
|
||||
agentPackageInventoryRequestSchema.parse({
|
||||
|
||||
@@ -281,7 +281,8 @@ export type AgentPackageInventory = z.infer<
|
||||
|
||||
export const agentPackageInventoryRequestSchema = z
|
||||
.object({
|
||||
refresh: z.boolean().default(false)
|
||||
refresh: z.boolean().default(false),
|
||||
localOnly: z.boolean().default(false)
|
||||
})
|
||||
.strict()
|
||||
|
||||
|
||||
@@ -1869,7 +1869,8 @@ export type DesktopApi = {
|
||||
listener: (status: SshHostAgentConnectionStatus) => void
|
||||
) => () => void
|
||||
getAgentPackageInventory: (
|
||||
refresh?: boolean
|
||||
refresh?: boolean,
|
||||
localOnly?: boolean
|
||||
) => Promise<AgentPackageInventory>
|
||||
downloadAgentPackage: (
|
||||
architecture: AgentArchitecture,
|
||||
|
||||
Reference in New Issue
Block a user