fix(agent-packages): show local inventory before refresh

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