Require Bitcoin version and pruning selection from the App Store
This commit is contained in:
@@ -189,3 +189,28 @@ live wallet data, host process IDs, or external network. Bitcoin/LND start times
|
||||
remained unchanged during isolated execution. Production helpers are unchanged;
|
||||
the namespace-specific command behavior is compiled only into unit tests.
|
||||
Release and ISO gates now use the isolated runner.
|
||||
|
||||
### Final backend deployment and App Store follow-up — 19:36 UTC
|
||||
|
||||
Full release harness passed: static/catalog checks, frontend type-check and
|
||||
1,117 frontend tests, cargo-check, and isolated backend suite (1,553 passed,
|
||||
four existing ignored). Final optimized backend built successfully; SHA256
|
||||
16a173129672cbb40c250446ec52ba4a9bd1974cbb3a4988f90c6f3187b7a1f7.
|
||||
Deployed to dev. Legacy :local LND companion automatically rebuilt at 19:35 UTC
|
||||
and restarted onto image 702c0cd88fb5c8a561c76dabdb96c40648dd62d401c78f2e10d4318b06f02abe.
|
||||
Served UI bytes match candidate source. Native Bitcoin/LND start times unchanged.
|
||||
|
||||
Actual desktop pruning screenshot exposed horizontal overflow; moved the
|
||||
explanation below the app header. The App Store uses Marketplace.vue, a separate
|
||||
install path from Discover.vue. Its first Install button bypassed the version
|
||||
modal. The browser check therefore sent an unintended Knots install request at
|
||||
19:28 UTC. Core remained running, no Knots container was created, and the full
|
||||
chain was not pruned. Removed only the newly created Knots installed-app record
|
||||
and newly created version config; preserved root-only rollback copies.
|
||||
|
||||
Marketplace now uses the shared version/pruning modal. Added integration tests
|
||||
for both Core and Knots: no install request until confirmation, selected version
|
||||
and pruning forwarded, cancellation sends no install request. Four Marketplace
|
||||
tests pass (three new plus existing refresh check). Further browser checks block
|
||||
package.install requests at their network boundary. Final frontend rebuild and
|
||||
post-fix live checks remain pending. Final paid-file follow-up is still pending.
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
:tier-label="getAppTier(app.id)"
|
||||
:install-blocked-reason="installBlockedReason(app.id)"
|
||||
@view="viewAppDetails"
|
||||
@install="app.source === 'local' ? installApp(app) : installCommunityApp(app)"
|
||||
@install="handleInstall(app)"
|
||||
@launch="launchInstalledApp"
|
||||
/>
|
||||
</div>
|
||||
@@ -153,7 +153,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Scrollable Apps Section -->
|
||||
|
||||
<InstallVersionModal
|
||||
:show="showInstallModal"
|
||||
:app-id="installModalApp?.id || ''"
|
||||
:app="installModalApp"
|
||||
@close="showInstallModal = false; installModalApp = null"
|
||||
@confirm="onInstallModalConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -175,11 +181,13 @@ import { useCollapsingHeaderTabs } from '@/composables/useCollapsingHeaderTabs'
|
||||
import { useContainersScanTimeout } from '@/composables/useContainersScanTimeout'
|
||||
import { useCachedResource } from '@/composables/useCachedResource'
|
||||
import RefreshIndicator from '@/components/RefreshIndicator.vue'
|
||||
import InstallVersionModal from '@/components/InstallVersionModal.vue'
|
||||
import { APP_STORE_CATEGORIES, APP_STORE_SECTIONS } from './appStoreCategories'
|
||||
import MarketplaceAppCard from './marketplace/MarketplaceAppCard.vue'
|
||||
import {
|
||||
type MarketplaceApp,
|
||||
INSTALLED_ALIASES,
|
||||
MULTI_VERSION_APP_IDS,
|
||||
getAppTier,
|
||||
categorizeCommunityApp,
|
||||
getCuratedAppList,
|
||||
@@ -206,6 +214,8 @@ const appStoreSections = computed(() => APP_STORE_SECTIONS)
|
||||
|
||||
// Installation state — uses global store so it persists across navigation
|
||||
const installingApps = server.installingApps
|
||||
const showInstallModal = ref(false)
|
||||
const installModalApp = ref<MarketplaceApp | null>(null)
|
||||
const electrumxArchiveWarning = 'You need a full archival bitcoin node before downloading ElectrumX'
|
||||
|
||||
function installToast(app: MarketplaceApp) {
|
||||
@@ -518,7 +528,34 @@ function failInstall(app: MarketplaceApp, err: unknown) {
|
||||
trackTimeout(() => { server.clearInstallProgress(app.id) }, 5000)
|
||||
}
|
||||
|
||||
async function installApp(app: MarketplaceApp) {
|
||||
function handleInstall(app: MarketplaceApp) {
|
||||
if (installingApps.has(app.id) || isInstalled(app.id)) return
|
||||
const blocked = installBlockedReason(app.id)
|
||||
if (blocked) {
|
||||
toast.error(blocked)
|
||||
return
|
||||
}
|
||||
if (MULTI_VERSION_APP_IDS.has(app.id)) {
|
||||
installModalApp.value = app
|
||||
showInstallModal.value = true
|
||||
return
|
||||
}
|
||||
startInstall(app)
|
||||
}
|
||||
|
||||
function startInstall(app: MarketplaceApp, version?: string, prune?: boolean) {
|
||||
if (app.source === 'local') void installApp(app, version, prune)
|
||||
else void installCommunityApp(app, version, prune)
|
||||
}
|
||||
|
||||
function onInstallModalConfirm(version: string, prune?: boolean) {
|
||||
const app = installModalApp.value
|
||||
showInstallModal.value = false
|
||||
installModalApp.value = null
|
||||
if (app) startInstall(app, version, prune)
|
||||
}
|
||||
|
||||
async function installApp(app: MarketplaceApp, versionOverride?: string, prune?: boolean) {
|
||||
if (installingApps.has(app.id) || isInstalled(app.id)) return
|
||||
const blocked = installBlockedReason(app.id)
|
||||
if (blocked) {
|
||||
@@ -536,7 +573,7 @@ async function installApp(app: MarketplaceApp) {
|
||||
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
||||
await rpcClient.call({
|
||||
method: 'package.install',
|
||||
params: { id: app.id, url: installUrl, version: app.version },
|
||||
params: { id: app.id, url: installUrl, version: versionOverride || app.version, ...(prune === undefined ? {} : { prune }) },
|
||||
timeout: 600000,
|
||||
})
|
||||
} catch (err) {
|
||||
@@ -545,7 +582,7 @@ async function installApp(app: MarketplaceApp) {
|
||||
}
|
||||
}
|
||||
|
||||
async function installCommunityApp(app: MarketplaceApp) {
|
||||
async function installCommunityApp(app: MarketplaceApp, versionOverride?: string, prune?: boolean) {
|
||||
if (installingApps.has(app.id) || isInstalled(app.id) || !app.dockerImage) return
|
||||
const blocked = installBlockedReason(app.id)
|
||||
if (blocked) {
|
||||
@@ -558,7 +595,7 @@ async function installCommunityApp(app: MarketplaceApp) {
|
||||
installToast(app)
|
||||
|
||||
try {
|
||||
const installParams: Record<string, unknown> = { id: app.id, dockerImage: app.dockerImage, version: app.version }
|
||||
const installParams: Record<string, unknown> = { id: app.id, dockerImage: app.dockerImage, version: versionOverride || app.version, ...(prune === undefined ? {} : { prune }) }
|
||||
if (app.containerConfig) installParams.containerConfig = app.containerConfig
|
||||
await rpcClient.call({
|
||||
method: 'package.install',
|
||||
|
||||
@@ -2,6 +2,9 @@ import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Marketplace from '../Marketplace.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import InstallVersionModal from '@/components/InstallVersionModal.vue'
|
||||
import MarketplaceAppCard from '../marketplace/MarketplaceAppCard.vue'
|
||||
|
||||
// Mirrors the CloudPeersRefresh.test.ts pattern (in-repo convention for
|
||||
// mounting a view directly with its heavier deps mocked at the module
|
||||
@@ -44,22 +47,37 @@ vi.mock('@/composables/useMarketplaceApp', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useToast: () => ({ success: vi.fn(), error: toastErrorMock, info: toastInfoMock }),
|
||||
useToast: () => ({ success: vi.fn(), error: toastErrorMock, info: toastInfoMock, action: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: {
|
||||
call: vi.fn(),
|
||||
marketplaceDiscover: vi.fn().mockResolvedValue({ apps: [] }),
|
||||
getPackageVersions: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../discover/curatedApps', () => ({
|
||||
fetchAppCatalog: vi.fn().mockResolvedValue({
|
||||
apps: ['bitcoin-core', 'bitcoin-knots'].map(id => ({
|
||||
id, title: id, version: '29.0', description: 'Bitcoin node',
|
||||
dockerImage: `registry.example/${id}:29.0`, source: 'community',
|
||||
})),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Marketplace tracer tab: background refresh failure (D-07)', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('ResizeObserver', vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn() })))
|
||||
routerPushMock.mockClear()
|
||||
toastErrorMock.mockClear()
|
||||
toastInfoMock.mockClear()
|
||||
vi.mocked(rpcClient.call).mockReset()
|
||||
vi.mocked(rpcClient.getPackageVersions).mockResolvedValue({
|
||||
supportsVersions: true, default: '29.0', bitcoinPrune: false,
|
||||
versions: [{ version: '29.0', default: true, deprecated: false, eol: null }],
|
||||
} as Awaited<ReturnType<typeof rpcClient.getPackageVersions>>)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -89,4 +107,38 @@ describe('Marketplace tracer tab: background refresh failure (D-07)', () => {
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it.each(['bitcoin-core', 'bitcoin-knots'])('requires the version modal before installing %s and forwards pruning', async (id) => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ blockchain_info: { pruned: false } }) }))
|
||||
const wrapper = mount(Marketplace, { global: { plugins: [createPinia()], stubs: { Teleport: true } } })
|
||||
await flushPromises()
|
||||
const card = wrapper.findAllComponents(MarketplaceAppCard).find(c => c.props('app').id === id)!
|
||||
expect(card.exists()).toBe(true)
|
||||
card.vm.$emit('install', card.props('app'))
|
||||
await flushPromises()
|
||||
const installs = () => vi.mocked(rpcClient.call).mock.calls.filter(([request]) => request.method === 'package.install')
|
||||
expect(installs()).toHaveLength(0)
|
||||
const modal = wrapper.findComponent(InstallVersionModal)
|
||||
expect(modal.props('show')).toBe(true)
|
||||
await modal.get('input[type="checkbox"]').setValue(true)
|
||||
await modal.get('button.glass-button-warning').trigger('click')
|
||||
await flushPromises()
|
||||
expect(installs()).toHaveLength(1)
|
||||
expect(installs()[0][0].params).toMatchObject({ id, version: '29.0', prune: true })
|
||||
expect(modal.props('show')).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('cancels Bitcoin selection without sending an installation request', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ blockchain_info: { pruned: false } }) }))
|
||||
const wrapper = mount(Marketplace, { global: { plugins: [createPinia()], stubs: { Teleport: true } } })
|
||||
await flushPromises()
|
||||
const card = wrapper.findAllComponents(MarketplaceAppCard).find(c => c.props('app').id === 'bitcoin-knots')!
|
||||
card.vm.$emit('install', card.props('app'))
|
||||
await flushPromises()
|
||||
wrapper.findComponent(InstallVersionModal).vm.$emit('close')
|
||||
await flushPromises()
|
||||
expect(vi.mocked(rpcClient.call).mock.calls.filter(([request]) => request.method === 'package.install')).toHaveLength(0)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user