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;
|
remained unchanged during isolated execution. Production helpers are unchanged;
|
||||||
the namespace-specific command behavior is compiled only into unit tests.
|
the namespace-specific command behavior is compiled only into unit tests.
|
||||||
Release and ISO gates now use the isolated runner.
|
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)"
|
:tier-label="getAppTier(app.id)"
|
||||||
:install-blocked-reason="installBlockedReason(app.id)"
|
:install-blocked-reason="installBlockedReason(app.id)"
|
||||||
@view="viewAppDetails"
|
@view="viewAppDetails"
|
||||||
@install="app.source === 'local' ? installApp(app) : installCommunityApp(app)"
|
@install="handleInstall(app)"
|
||||||
@launch="launchInstalledApp"
|
@launch="launchInstalledApp"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +153,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- End Scrollable Apps Section -->
|
<!-- End Scrollable Apps Section -->
|
||||||
|
<InstallVersionModal
|
||||||
|
:show="showInstallModal"
|
||||||
|
:app-id="installModalApp?.id || ''"
|
||||||
|
:app="installModalApp"
|
||||||
|
@close="showInstallModal = false; installModalApp = null"
|
||||||
|
@confirm="onInstallModalConfirm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -175,11 +181,13 @@ import { useCollapsingHeaderTabs } from '@/composables/useCollapsingHeaderTabs'
|
|||||||
import { useContainersScanTimeout } from '@/composables/useContainersScanTimeout'
|
import { useContainersScanTimeout } from '@/composables/useContainersScanTimeout'
|
||||||
import { useCachedResource } from '@/composables/useCachedResource'
|
import { useCachedResource } from '@/composables/useCachedResource'
|
||||||
import RefreshIndicator from '@/components/RefreshIndicator.vue'
|
import RefreshIndicator from '@/components/RefreshIndicator.vue'
|
||||||
|
import InstallVersionModal from '@/components/InstallVersionModal.vue'
|
||||||
import { APP_STORE_CATEGORIES, APP_STORE_SECTIONS } from './appStoreCategories'
|
import { APP_STORE_CATEGORIES, APP_STORE_SECTIONS } from './appStoreCategories'
|
||||||
import MarketplaceAppCard from './marketplace/MarketplaceAppCard.vue'
|
import MarketplaceAppCard from './marketplace/MarketplaceAppCard.vue'
|
||||||
import {
|
import {
|
||||||
type MarketplaceApp,
|
type MarketplaceApp,
|
||||||
INSTALLED_ALIASES,
|
INSTALLED_ALIASES,
|
||||||
|
MULTI_VERSION_APP_IDS,
|
||||||
getAppTier,
|
getAppTier,
|
||||||
categorizeCommunityApp,
|
categorizeCommunityApp,
|
||||||
getCuratedAppList,
|
getCuratedAppList,
|
||||||
@@ -206,6 +214,8 @@ const appStoreSections = computed(() => APP_STORE_SECTIONS)
|
|||||||
|
|
||||||
// Installation state — uses global store so it persists across navigation
|
// Installation state — uses global store so it persists across navigation
|
||||||
const installingApps = server.installingApps
|
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'
|
const electrumxArchiveWarning = 'You need a full archival bitcoin node before downloading ElectrumX'
|
||||||
|
|
||||||
function installToast(app: MarketplaceApp) {
|
function installToast(app: MarketplaceApp) {
|
||||||
@@ -518,7 +528,34 @@ function failInstall(app: MarketplaceApp, err: unknown) {
|
|||||||
trackTimeout(() => { server.clearInstallProgress(app.id) }, 5000)
|
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
|
if (installingApps.has(app.id) || isInstalled(app.id)) return
|
||||||
const blocked = installBlockedReason(app.id)
|
const blocked = installBlockedReason(app.id)
|
||||||
if (blocked) {
|
if (blocked) {
|
||||||
@@ -536,7 +573,7 @@ async function installApp(app: MarketplaceApp) {
|
|||||||
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
||||||
await rpcClient.call({
|
await rpcClient.call({
|
||||||
method: 'package.install',
|
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,
|
timeout: 600000,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} 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
|
if (installingApps.has(app.id) || isInstalled(app.id) || !app.dockerImage) return
|
||||||
const blocked = installBlockedReason(app.id)
|
const blocked = installBlockedReason(app.id)
|
||||||
if (blocked) {
|
if (blocked) {
|
||||||
@@ -558,7 +595,7 @@ async function installCommunityApp(app: MarketplaceApp) {
|
|||||||
installToast(app)
|
installToast(app)
|
||||||
|
|
||||||
try {
|
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
|
if (app.containerConfig) installParams.containerConfig = app.containerConfig
|
||||||
await rpcClient.call({
|
await rpcClient.call({
|
||||||
method: 'package.install',
|
method: 'package.install',
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { flushPromises, mount } from '@vue/test-utils'
|
|||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import Marketplace from '../Marketplace.vue'
|
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
|
// Mirrors the CloudPeersRefresh.test.ts pattern (in-repo convention for
|
||||||
// mounting a view directly with its heavier deps mocked at the module
|
// mounting a view directly with its heavier deps mocked at the module
|
||||||
@@ -44,22 +47,37 @@ vi.mock('@/composables/useMarketplaceApp', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/composables/useToast', () => ({
|
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', () => ({
|
vi.mock('@/api/rpc-client', () => ({
|
||||||
rpcClient: {
|
rpcClient: {
|
||||||
call: vi.fn(),
|
call: vi.fn(),
|
||||||
marketplaceDiscover: vi.fn().mockResolvedValue({ apps: [] }),
|
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)', () => {
|
describe('Marketplace tracer tab: background refresh failure (D-07)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubGlobal('ResizeObserver', vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn() })))
|
vi.stubGlobal('ResizeObserver', vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn() })))
|
||||||
routerPushMock.mockClear()
|
routerPushMock.mockClear()
|
||||||
toastErrorMock.mockClear()
|
toastErrorMock.mockClear()
|
||||||
toastInfoMock.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(() => {
|
afterEach(() => {
|
||||||
@@ -89,4 +107,38 @@ describe('Marketplace tracer tab: background refresh failure (D-07)', () => {
|
|||||||
|
|
||||||
wrapper.unmount()
|
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