archy/neode-ui/src/views/apps/__tests__/appsConfig.test.ts
archipelago d2d2b9dd68 fix(apps): classify by declared UI — UI apps to My Apps, headless to Websites (#45)
Per the rule that only front-end apps with a UI belong in "My Apps"
(databases/backends/headless go to Websites), make the manifest's
interfaces.main.ui the deciding signal. isWebsitePackage now treats any
package that declares a UI as an app even when it isn't in the curated
APP_CATEGORY_MAP, and falls through headless LAN-reachable packages to
Websites. Additive — service-by-name infra and curated known apps are
unchanged, so no currently-correct app moves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 06:09:46 -04:00

109 lines
4.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { ref } from 'vue'
import { PackageState, type PackageDataEntry } from '@/types/api'
import { canLaunch, filterEntriesForTab, hasFrontendUi, isServiceContainer, isServicePackage, isWebsitePackage, launchBlockedReason, resolveAppIcon, useCategoriesWithApps } from '../appsConfig'
function makePkg(id: string, title: string, category: string): PackageDataEntry {
return {
state: PackageState.Running,
manifest: {
id,
title,
version: '1.0.0',
description: { short: '', long: '' },
'release-notes': '',
license: '',
'wrapper-repo': '',
'upstream-repo': '',
'support-site': '',
'marketing-site': '',
'donation-url': null,
category,
} as unknown as PackageDataEntry['manifest'],
'static-files': { license: '', instructions: '', icon: '' },
}
}
describe('appsConfig service filtering', () => {
it('treats bitcoin stack UI sidecars as services', () => {
expect(isServiceContainer('bitcoin-ui')).toBe(true)
expect(isServiceContainer('lnd-ui')).toBe(true)
expect(isServiceContainer('electrs-ui')).toBe(true)
})
it('treats container aliases as services even with non-service keys', () => {
const aliasPkg = makePkg('bitcoin-ui', 'Bitcoin UI', 'money')
expect(isServicePackage('core-lnd-ui', aliasPkg)).toBe(true)
})
it('removes service-only categories from app category tabs', () => {
const packages = ref<Record<string, PackageDataEntry>>({
'core-bitcoin-ui': makePkg('bitcoin-ui', 'Bitcoin UI', 'money'),
'filebrowser': makePkg('filebrowser', 'File Browser', 'data'),
})
const allCategories = ref([
{ id: 'all', name: 'All' },
{ id: 'money', name: 'Money' },
{ id: 'data', name: 'Data' },
])
const visible = useCategoriesWithApps(packages, allCategories)
expect(visible.value.map(c => c.id)).toEqual(['all', 'data'])
})
it('filters apps tab by category using manifest-aware service checks', () => {
const entries: Array<[string, PackageDataEntry]> = [
['core-bitcoin-ui', makePkg('bitcoin-ui', 'Bitcoin UI', 'money')],
['filebrowser', makePkg('filebrowser', 'File Browser', 'data')],
['btcpay-server', makePkg('btcpay-server', 'BTCPay', 'commerce')],
]
const appsAll = filterEntriesForTab(entries, 'apps', 'all')
expect(appsAll.map(([id]) => id)).toEqual(['filebrowser', 'btcpay-server'])
const appsData = filterEntriesForTab(entries, 'apps', 'data')
expect(appsData.map(([id]) => id)).toEqual(['filebrowser'])
})
it('routes service aliases into services tab and excludes user apps', () => {
const entries: Array<[string, PackageDataEntry]> = [
['core-lnd-ui', makePkg('lnd-ui', 'LND UI', 'money')],
['grafana', makePkg('grafana', 'Grafana', 'data')],
]
const services = filterEntriesForTab(entries, 'services', 'all')
expect(services.map(([id]) => id)).toEqual(['core-lnd-ui'])
})
it('falls back to packaged app icon when static icon token is not a path', () => {
const pkg = makePkg('gitea', 'Gitea', 'dev')
pkg['static-files']!.icon = 'git-branch'
expect(resolveAppIcon('gitea', pkg)).toBe('/assets/img/app-icons/gitea.svg')
})
it('classifies an unknown app by whether its manifest declares a UI (#45)', () => {
// Headless: a LAN address but no declared UI → Website.
const headless = makePkg('some-backend', 'Some Backend', 'other')
headless.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:9000' } } } as unknown as PackageDataEntry['installed']
expect(hasFrontendUi(headless)).toBe(false)
expect(isWebsitePackage('some-backend', headless)).toBe(true)
// Front-end app: declares interfaces.main.ui → My Apps even when not in the
// curated category map.
const uiApp = makePkg('some-ui-app', 'Some UI App', 'other')
;(uiApp.manifest as unknown as Record<string, unknown>).interfaces = { main: { ui: 'http://localhost:9001' } }
uiApp.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:9001' } } } as unknown as PackageDataEntry['installed']
expect(hasFrontendUi(uiApp)).toBe(true)
expect(isWebsitePackage('some-ui-app', uiApp)).toBe(false)
})
it('explains that Fedimint waits for Bitcoin sync before Guardian starts', () => {
const pkg = makePkg('fedimint', 'Fedimint', 'money')
pkg.state = PackageState.Starting
pkg.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:8175' } } } as unknown as PackageDataEntry['installed']
expect(launchBlockedReason('fedimint', pkg)).toContain('Bitcoin')
expect(canLaunch(pkg)).toBe(true)
})
})