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
|
} 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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user