Compare commits

...

4 Commits

Author SHA1 Message Date
Matt Miller
dac9e5c25f test: cover no-version install branch; use function declarations
- getPackUpdateStatus/isNightlyVersion as function declarations (AGENTS.md 24)
- add getPackUpdateStatus test for an installed pack with an undefined version
- mirror the real store (undefined version) in the not-installed composable test
2026-07-13 21:35:01 -07:00
Matt Miller
e45e7e45ee test: cover usePackUpdateStatus composable
Adds behavioral coverage for the update-status composable (update
detection, nightly handling, and reactivity to source changes),
raising patch coverage for the extracted helper's call sites.
2026-07-02 13:08:08 -07:00
Matt Miller
c97298a4e8 fix: guard against non-semver latest version in pack update status
A malformed registry latest_version (e.g. a git hash) would reach
semver.compare unvalidated and throw TypeError: Invalid Version,
breaking every update-status computed and the outdated-pack filters.
Validate latestVersion is semver before comparing.
2026-07-02 12:51:32 -07:00
Matt Miller
ae328319d3 refactor: extract shared pack update-status helper
Consolidate the duplicated 'is installed pack outdated' semver logic that
was reimplemented across usePackUpdateStatus, useUpdateAvailableNodes, and
useManagerDisplayPacks (filterOutdated), plus the open-coded nightly check in
usePacksSelection, into a single getPackUpdateStatus helper (with a standalone
isNightlyVersion predicate) in manager utils. Behavior-preserving.
2026-07-02 12:38:45 -07:00
7 changed files with 287 additions and 62 deletions

View File

@@ -0,0 +1,108 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn()
}))
type NodePack = components['schemas']['Node']
type ManagerStoreReturn = ReturnType<typeof useComfyManagerStore>
const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore)
const mockIsPackInstalled = vi.fn()
const mockIsPackEnabled = vi.fn()
const mockGetInstalledPackVersion = vi.fn()
function makePack(overrides: Partial<NodePack> = {}): NodePack {
return {
id: 'pack-1',
name: 'Pack',
latest_version: { version: '2.0.0' },
...overrides
} as NodePack
}
beforeEach(() => {
vi.clearAllMocks()
mockIsPackInstalled.mockReturnValue(true)
mockIsPackEnabled.mockReturnValue(true)
mockGetInstalledPackVersion.mockReturnValue('1.0.0')
mockUseComfyManagerStore.mockReturnValue({
isPackInstalled: mockIsPackInstalled,
isPackEnabled: mockIsPackEnabled,
getInstalledPackVersion: mockGetInstalledPackVersion
} as Partial<ManagerStoreReturn> as ManagerStoreReturn)
})
describe('usePackUpdateStatus', () => {
it('reports an update when the latest release is newer', () => {
const { isUpdateAvailable, installedVersion, latestVersion } =
usePackUpdateStatus(makePack())
expect(isUpdateAvailable.value).toBe(true)
expect(installedVersion.value).toBe('1.0.0')
expect(latestVersion.value).toBe('2.0.0')
})
it('reports no update when installed matches latest', () => {
mockGetInstalledPackVersion.mockReturnValue('2.0.0')
const { isUpdateAvailable } = usePackUpdateStatus(makePack())
expect(isUpdateAvailable.value).toBe(false)
})
it('reports no update when the pack is not installed', () => {
mockIsPackInstalled.mockReturnValue(false)
mockGetInstalledPackVersion.mockReturnValue(undefined)
const { isUpdateAvailable } = usePackUpdateStatus(makePack())
expect(isUpdateAvailable.value).toBe(false)
})
it('treats a non-semver installed version as a nightly build', () => {
mockGetInstalledPackVersion.mockReturnValue('abc1234')
const { isNightlyPack, isUpdateAvailable, canTryNightlyUpdate } =
usePackUpdateStatus(makePack())
expect(isNightlyPack.value).toBe(true)
expect(isUpdateAvailable.value).toBe(false)
expect(canTryNightlyUpdate.value).toBe(true)
})
it('only allows a nightly update when installed and enabled', () => {
mockGetInstalledPackVersion.mockReturnValue('abc1234')
mockIsPackEnabled.mockReturnValue(false)
const { canTryNightlyUpdate } = usePackUpdateStatus(makePack())
expect(canTryNightlyUpdate.value).toBe(false)
})
it('reports no update when the registry omits a latest version', () => {
const { isUpdateAvailable } = usePackUpdateStatus(
makePack({ latest_version: undefined })
)
expect(isUpdateAvailable.value).toBe(false)
})
it('recomputes when the source pack changes', async () => {
const source = ref(makePack({ latest_version: { version: '1.0.0' } }))
const { isUpdateAvailable } = usePackUpdateStatus(source)
expect(isUpdateAvailable.value).toBe(false)
source.value = makePack({ latest_version: { version: '3.0.0' } })
await nextTick()
expect(isUpdateAvailable.value).toBe(true)
})
})

View File

@@ -1,42 +1,30 @@
import type { MaybeRefOrGetter } from 'vue'
import { computed, toValue } from 'vue'
import { compare, valid } from 'semver'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { getPackUpdateStatus } from '@/workbench/extensions/manager/utils/packUpdateStatus'
export const usePackUpdateStatus = (
nodePackSource: MaybeRefOrGetter<components['schemas']['Node']>
) => {
const { isPackInstalled, isPackEnabled, getInstalledPackVersion } =
useComfyManagerStore()
const managerStore = useComfyManagerStore()
// Use toValue to unwrap the source reactively inside computeds
const nodePack = computed(() => toValue(nodePackSource))
const isInstalled = computed(() => isPackInstalled(nodePack.value?.id))
const isEnabled = computed(() => isPackEnabled(nodePack.value?.id))
const installedVersion = computed(() =>
getInstalledPackVersion(nodePack.value?.id ?? '')
)
const latestVersion = computed(() => nodePack.value?.latest_version?.version)
const isNightlyPack = computed(
() => !!installedVersion.value && !valid(installedVersion.value)
const status = computed(() =>
getPackUpdateStatus(nodePack.value, managerStore)
)
const isUpdateAvailable = computed(() => {
if (
!isInstalled.value ||
isNightlyPack.value ||
!latestVersion.value ||
!installedVersion.value
) {
return false
}
return compare(latestVersion.value, installedVersion.value) > 0
})
const isInstalled = computed(() => status.value.isInstalled)
const isEnabled = computed(() =>
managerStore.isPackEnabled(nodePack.value?.id)
)
const installedVersion = computed(() => status.value.installedVersion)
const latestVersion = computed(() => status.value.latestVersion)
const isNightlyPack = computed(() => status.value.isNightly)
const isUpdateAvailable = computed(() => status.value.isUpdateAvailable)
/**
* Nightly packs can always "try update" since we cannot compare git hashes

View File

@@ -1,9 +1,9 @@
import { valid } from 'semver'
import { computed } from 'vue'
import type { Ref } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { isNightlyVersion } from '@/workbench/extensions/manager/utils/packUpdateStatus'
type NodePack = components['schemas']['Node']
@@ -50,9 +50,8 @@ export function usePacksSelection(nodePacks: Ref<NodePack[]>) {
installedPacks.value.filter((pack) => {
if (!pack.id) return false
const version = managerStore.getInstalledPackVersion(pack.id)
const isNightly = !!version && !valid(version)
const isEnabled = managerStore.isPackEnabled(pack.id)
return isNightly && isEnabled
return isNightlyVersion(version) && isEnabled
})
)

View File

@@ -1,9 +1,9 @@
import { compare, valid } from 'semver'
import { computed, onMounted } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import { useInstalledPacks } from '@/workbench/extensions/manager/composables/nodePack/useInstalledPacks'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { getPackUpdateStatus } from '@/workbench/extensions/manager/utils/packUpdateStatus'
/**
* Composable to find NodePacks that have updates available
@@ -14,24 +14,8 @@ export const useUpdateAvailableNodes = () => {
const { installedPacks, isLoading, error, startFetchInstalled } =
useInstalledPacks()
// Check if a pack has updates available (same logic as usePackUpdateStatus)
const isOutdatedPack = (pack: components['schemas']['Node']) => {
const isInstalled = comfyManagerStore.isPackInstalled(pack?.id)
if (!isInstalled) return false
const installedVersion = comfyManagerStore.getInstalledPackVersion(
pack.id ?? ''
)
const latestVersion = pack.latest_version?.version
const isNightlyPack = !!installedVersion && !valid(installedVersion)
if (isNightlyPack || !latestVersion || !installedVersion) {
return false
}
return compare(latestVersion, installedVersion) > 0
}
const isOutdatedPack = (pack: components['schemas']['Node']) =>
getPackUpdateStatus(pack, comfyManagerStore).isUpdateAvailable
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
packs.filter(isOutdatedPack)

View File

@@ -1,6 +1,5 @@
import { whenever } from '@vueuse/core'
import { orderBy } from 'es-toolkit/compat'
import { compare, valid } from 'semver'
import type { Ref } from 'vue'
import { computed } from 'vue'
@@ -11,6 +10,7 @@ import { useWorkflowPacks } from '@/workbench/extensions/manager/composables/nod
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import { getPackUpdateStatus } from '@/workbench/extensions/manager/utils/packUpdateStatus'
type NodePack = components['schemas']['Node']
@@ -72,21 +72,9 @@ export function useManagerDisplayPacks(
)
const filterOutdated = (packs: NodePack[]) =>
packs.filter((p) => {
const installedVersion = comfyManagerStore.getInstalledPackVersion(
p.id ?? ''
)
const latestVersion = p.latest_version?.version
if (
!comfyManagerStore.isPackInstalled(p.id) ||
!installedVersion ||
!latestVersion ||
!valid(installedVersion) // nightly builds
) {
return false
}
return compare(latestVersion, installedVersion) > 0
})
packs.filter(
(p) => getPackUpdateStatus(p, comfyManagerStore).isUpdateAvailable
)
// Data fetching triggers using whenever
const needsInstalledPacks = computed(() =>

View File

@@ -0,0 +1,104 @@
import { describe, expect, it } from 'vitest'
import type { components } from '@/types/comfyRegistryTypes'
import type { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import {
getPackUpdateStatus,
isNightlyVersion
} from '@/workbench/extensions/manager/utils/packUpdateStatus'
type NodePack = components['schemas']['Node']
type ComfyManagerStore = ReturnType<typeof useComfyManagerStore>
const createPack = (id: string | undefined, latestVersion?: string): NodePack =>
({
id,
name: `Pack ${id}`,
latest_version: latestVersion ? { version: latestVersion } : undefined
}) as NodePack
const createStore = (
installed: Record<string, string | undefined>
): ComfyManagerStore =>
({
isPackInstalled: (id: string | undefined) =>
id !== undefined && id in installed,
getInstalledPackVersion: (id: string) => installed[id]
}) as unknown as ComfyManagerStore
describe('isNightlyVersion', () => {
it('is true for a non-semver git hash version', () => {
expect(isNightlyVersion('nightly-abc123')).toBe(true)
})
it('is false for a valid semver version', () => {
expect(isNightlyVersion('1.2.3')).toBe(false)
})
it('is false for an undefined or empty version', () => {
expect(isNightlyVersion(undefined)).toBe(false)
expect(isNightlyVersion('')).toBe(false)
})
})
describe('getPackUpdateStatus', () => {
it('reports an update available when the latest version is newer', () => {
const store = createStore({ 'pack-1': '1.0.0' })
const status = getPackUpdateStatus(createPack('pack-1', '2.0.0'), store)
expect(status).toEqual({
isInstalled: true,
installedVersion: '1.0.0',
latestVersion: '2.0.0',
isNightly: false,
isUpdateAvailable: true
})
})
it('reports no update when the installed version is up to date', () => {
const store = createStore({ 'pack-1': '2.0.0' })
const status = getPackUpdateStatus(createPack('pack-1', '2.0.0'), store)
expect(status.isUpdateAvailable).toBe(false)
})
it('never reports an update for a nightly (non-semver) install', () => {
const store = createStore({ 'pack-1': 'nightly-abc123' })
const status = getPackUpdateStatus(createPack('pack-1', '2.0.0'), store)
expect(status.isNightly).toBe(true)
expect(status.isUpdateAvailable).toBe(false)
})
it('reports no update when the pack is not installed', () => {
const store = createStore({})
const status = getPackUpdateStatus(createPack('pack-1', '2.0.0'), store)
expect(status.isInstalled).toBe(false)
expect(status.isUpdateAvailable).toBe(false)
})
it('reports no update when the latest version is not valid semver', () => {
const store = createStore({ 'pack-1': '1.0.0' })
const status = getPackUpdateStatus(createPack('pack-1', 'deadbeef'), store)
expect(status.isUpdateAvailable).toBe(false)
})
it('reports no update for an installed pack with no known version', () => {
const store = createStore({ 'pack-1': undefined })
const status = getPackUpdateStatus(createPack('pack-1', '2.0.0'), store)
expect(status.isInstalled).toBe(true)
expect(status.installedVersion).toBeUndefined()
expect(status.isUpdateAvailable).toBe(false)
})
it('reports no update when the pack has no latest version', () => {
const store = createStore({ 'pack-1': '1.0.0' })
const status = getPackUpdateStatus(createPack('pack-1'), store)
expect(status.latestVersion).toBeUndefined()
expect(status.isUpdateAvailable).toBe(false)
})
})

View File

@@ -0,0 +1,54 @@
import { compare, valid } from 'semver'
import type { components } from '@/types/comfyRegistryTypes'
import type { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
type NodePack = components['schemas']['Node']
type ComfyManagerStore = ReturnType<typeof useComfyManagerStore>
interface PackUpdateStatus {
isInstalled: boolean
installedVersion: string | undefined
latestVersion: string | undefined
isNightly: boolean
isUpdateAvailable: boolean
}
/**
* A pack is a nightly build when its installed version is not valid semver
* (a git hash), so it cannot be compared against the latest release version.
*/
export function isNightlyVersion(version: string | undefined): boolean {
return !!version && !valid(version)
}
/**
* Derives the update status of a pack from its installed version and the
* latest release version advertised by the registry. A nightly build is never
* "outdated" since git hashes cannot be ordered.
*/
export function getPackUpdateStatus(
pack: NodePack | undefined,
managerStore: ComfyManagerStore
): PackUpdateStatus {
const isInstalled = managerStore.isPackInstalled(pack?.id)
const installedVersion = managerStore.getInstalledPackVersion(pack?.id ?? '')
const latestVersion = pack?.latest_version?.version
const isNightly = isNightlyVersion(installedVersion)
const isUpdateAvailable =
isInstalled &&
!isNightly &&
!!installedVersion &&
!!latestVersion &&
!!valid(latestVersion) &&
compare(latestVersion, installedVersion) > 0
return {
isInstalled,
installedVersion,
latestVersion,
isNightly,
isUpdateAvailable
}
}