Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:50 +00:00
commit b67e1527a2
2068 changed files with 472303 additions and 0 deletions
@@ -0,0 +1,68 @@
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import AppSessionFrame from '../AppSessionFrame.vue'
// Regression cover for the operator-reported defect: a container that is up
// but has not finished warming up (bitcoind serving RPC -28, lnd before the
// wallet unlocks) rendered the hard "App not reachable" failure copy for the
// whole warm-up window. The retry machinery already tolerated it — only the
// headline lied.
function mountFrame(props: Record<string, unknown> = {}) {
return mount(AppSessionFrame, {
props: {
appUrl: 'http://localhost:8332/',
appId: 'bitcoin-knots',
appTitle: 'Bitcoin',
appIcon: '/icons/bitcoin.png',
loading: false,
iframeBlocked: true,
mustOpenNewTab: false,
autoRetryCount: 1,
refreshKey: 0,
...props,
},
global: { stubs: { AppLoadingScreen: true, Transition: false } },
})
}
describe('AppSessionFrame warm-up state', () => {
it('reads as starting, not unreachable, while the container is warming up', () => {
const text = mountFrame({ warmingUp: true }).text()
expect(text).toContain('Bitcoin is starting…')
expect(text).not.toContain('App not reachable')
})
it('says the container is running so the copy does not imply it is stopped', () => {
const text = mountFrame({ warmingUp: true }).text()
expect(text).toContain("container is running but hasn't finished warming up")
expect(text).not.toContain('the container is stopped')
})
it('still surfaces the automatic re-check while warming up', () => {
expect(mountFrame({ warmingUp: true, autoRetryCount: 3 }).text()).toContain(
'Checking again automatically (3)',
)
})
it('reverts to the real failure once warm-up is over (retries exhausted)', () => {
const text = mountFrame({ warmingUp: false, autoRetryCount: 6 }).text()
expect(text).toContain('App not reachable')
expect(text).not.toContain('is starting…')
})
it('leaves the explicit blocked-reason path untouched', () => {
const text = mountFrame({
warmingUp: false,
blockedReason: 'Waiting for Bitcoin to finish syncing.',
blockedTitle: 'Waiting for Bitcoin sync',
}).text()
expect(text).toContain('Waiting for Bitcoin sync')
expect(text).not.toContain('App not reachable')
})
it('leaves the new-tab path untouched', () => {
const text = mountFrame({ warmingUp: false, mustOpenNewTab: true }).text()
expect(text).toContain('This app opens in a new tab')
})
})
@@ -0,0 +1,55 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { appOrigin, matchPageScheme } from '../appSessionConfig'
// An HTTPS dashboard cannot embed an HTTP app frame — browsers block it as
// mixed content — so the app origin has to follow the page's scheme. Plain-HTTP
// nodes must be completely unaffected, which is what most of these pin.
function setLocation(protocol: string, hostname: string) {
Object.defineProperty(window, 'location', {
value: { protocol, hostname },
writable: true,
configurable: true,
})
}
afterEach(() => vi.unstubAllGlobals())
describe('appOrigin', () => {
it('stays on http for an http dashboard', () => {
setLocation('http:', 'test-node.local')
expect(appOrigin(8334)).toBe('http://test-node.local:8334')
})
it('follows an https dashboard onto the app port', () => {
setLocation('https:', 'test-node.local')
expect(appOrigin(8334)).toBe('https://test-node.local:8334')
})
it('keeps the hostname the user actually typed, not a fixed name', () => {
setLocation('https:', '100.64.0.5')
expect(appOrigin(3000)).toBe('https://100.64.0.5:3000')
})
})
describe('matchPageScheme', () => {
it('leaves backend-reported http URLs alone on an http page', () => {
setLocation('http:', 'node')
expect(matchPageScheme('http://node:8080/app')).toBe('http://node:8080/app')
})
it('upgrades a backend-reported http URL on an https page', () => {
setLocation('https:', 'node')
expect(matchPageScheme('http://node:8080/app')).toBe('https://node:8080/app')
})
it('does not touch anything but the scheme', () => {
setLocation('https:', 'node')
expect(matchPageScheme('http://node:8080/a/b?c=1#d')).toBe('https://node:8080/a/b?c=1#d')
})
it('leaves an already-https URL untouched', () => {
setLocation('https:', 'node')
expect(matchPageScheme('https://node:8080/app')).toBe('https://node:8080/app')
})
})
@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest'
import { NEW_TAB_APPS, resolveAppUrl } from '../appSessionConfig'
import { GENERATED_NEW_TAB_APPS } from '../generatedAppSessionConfig'
describe('appSessionConfig', () => {
it('keeps manifest-owned new-tab apps marked on every viewport', () => {
expect(NEW_TAB_APPS.has('btcpay-server')).toBe(true)
expect(NEW_TAB_APPS.has('photoprism')).toBe(true)
expect(GENERATED_NEW_TAB_APPS.has('photoprism')).toBe(true)
})
it('keeps frontend-only new-tab overrides for apps without generated metadata', () => {
expect(NEW_TAB_APPS.has('tailscale')).toBe(true)
expect(GENERATED_NEW_TAB_APPS.has('tailscale')).toBe(false)
})
it('resolves direct app ports against the current browser host', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.0.2.10' },
writable: true,
configurable: true,
})
expect(resolveAppUrl('mempool')).toBe('http://192.0.2.10:4080')
expect(resolveAppUrl('indeedhub')).toBe('http://192.0.2.10:7778')
expect(resolveAppUrl('botfights')).toBe('http://192.0.2.10:9100')
})
it('uses manifest-generated launch ports for apps outside the manual override list', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.0.2.10' },
writable: true,
configurable: true,
})
// did-wallet's manifest publishes host port 8088 (apps/did-wallet/
// manifest.yml) — assert against the manifest-generated value, which is
// exactly what this test exists to protect.
expect(resolveAppUrl('did-wallet')).toBe('http://192.0.2.10:8088')
})
it('does not treat service-only tcp ports as web launch surfaces', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.0.2.10' },
writable: true,
configurable: true,
})
expect(resolveAppUrl('meshtastic')).toBe('')
})
it('keeps NetBird on the unified dashboard proxy port', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.0.2.10' },
writable: true,
configurable: true,
})
expect(resolveAppUrl('netbird', undefined, 'http://localhost:8086')).toBe('http://192.0.2.10:8087')
})
it('uses backend runtime URLs for apps with dynamic launch surfaces', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.0.2.10' },
writable: true,
configurable: true,
})
expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:18083')).toBe('http://192.0.2.10:18083')
})
})