ADD - expand phone application platform

Add the Companies app, resilient call handling, custom app registry, vendor compatibility, storage policies, frontend bridge, tests, documentation, config, locale, and SQL migrations.
This commit is contained in:
Leon.Schmidt
2026-08-11 00:22:39 +02:00
parent 72e5f91e02
commit 42ac955052
77 changed files with 17630 additions and 333 deletions
+268
View File
@@ -0,0 +1,268 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
createCustomAppBridgeRequestHandler,
getSkyPhoneAppCapabilities,
shouldReportCustomAppReady,
type CustomAppBridgeNotification,
type CustomAppBridgeTarget,
} from '@/utils/customAppBridge'
const source = {
capabilities: [] as string[],
id: 'source-app',
}
const createNotification =
vi.fn<(input: CustomAppBridgeNotification) => string | null>()
const prepareExternalOpen = vi.fn<(appId: string, data?: unknown) => boolean>()
const resolveTarget = vi.fn<(appId: string) => CustomAppBridgeTarget | null>()
const storageCall = vi.fn()
function createHandler() {
return createCustomAppBridgeRequestHandler({
createNotification,
getSourceApp: () => source,
prepareExternalOpen,
resolveTarget,
storageCall,
})
}
describe('custom app bridge', () => {
beforeEach(() => {
source.capabilities = []
createNotification.mockReset().mockReturnValue('notification-1')
prepareExternalOpen.mockReset().mockReturnValue(true)
resolveTarget.mockReset().mockReturnValue(null)
storageCall.mockReset().mockResolvedValue({ success: true })
})
it('advertises only implemented methods granted by manifest permissions', () => {
expect(
getSkyPhoneAppCapabilities([
'app.close',
'device.storage',
'notifications',
'location.read',
]),
).toEqual([
'app.close',
'device.storage.get',
'device.storage.set',
'notification.create',
])
})
it('reports ready lifecycle from load only for legacy frames', () => {
expect(shouldReportCustomAppReady('legacy', 'frame-load')).toBe(true)
expect(shouldReportCustomAppReady('legacy', 'bridge-ready')).toBe(false)
expect(shouldReportCustomAppReady('sky', 'frame-load')).toBe(false)
expect(shouldReportCustomAppReady('sky', 'bridge-ready')).toBe(true)
})
it('passes validated storage data and server response data through', async () => {
source.capabilities = ['device.storage']
storageCall.mockResolvedValue({
data: { exists: true, revision: 4, value: { enabled: true } },
success: true,
})
const handler = createHandler()
const result = await handler.handle({
method: 'device.storage.get',
payload: { key: 'settings.main' },
requestId: 'storage-get',
})
expect(storageCall).toHaveBeenCalledWith('custom-app:storage:get', {
appId: 'source-app',
key: 'settings.main',
})
expect(result?.response).toEqual({
data: { exists: true, revision: 4, value: { enabled: true } },
requestId: 'storage-get',
success: true,
})
})
it('validates storage writes and deduplicates request ids', async () => {
source.capabilities = ['device.storage']
const handler = createHandler()
const request = {
method: 'device.storage.set',
payload: { key: 'profile', revision: 2, value: { color: 'blue' } },
requestId: 'storage-set',
}
const first = await handler.handle(request)
const duplicate = await handler.handle(request)
const invalid = await handler.handle({
method: 'device.storage.set',
payload: { key: '../profile', revision: -1, value: Number.NaN },
requestId: 'storage-invalid',
})
expect(first?.response.success).toBe(true)
expect(duplicate).toBeNull()
expect(invalid?.response).toMatchObject({
error: 'invalid_storage_request',
success: false,
})
expect(storageCall).toHaveBeenCalledWith('custom-app:storage:set', {
appId: 'source-app',
key: 'profile',
revision: 2,
value: { color: 'blue' },
})
expect(storageCall).toHaveBeenCalledOnce()
})
it('bounds the request-id deduplication window', async () => {
const handler = createHandler()
const first = await handler.handle({
method: 'unknown',
requestId: 'oldest-request',
})
for (let index = 0; index < 512; index += 1) {
await handler.handle({
method: 'unknown',
requestId: `request-${index}`,
})
}
const afterEviction = await handler.handle({
method: 'unknown',
requestId: 'oldest-request',
})
expect(first?.response.error).toBe('unsupported_method')
expect(afterEviction?.response.error).toBe('unsupported_method')
})
it('requires storage and close permissions', async () => {
const handler = createHandler()
const storage = await handler.handle({
method: 'device.storage.get',
payload: { key: 'profile' },
requestId: 'denied-storage',
})
const close = await handler.handle({
method: 'app.close',
requestId: 'denied-close',
})
expect(storage?.response.error).toBe('permission_denied')
expect(close?.response.error).toBe('permission_denied')
expect(close?.effect).toBeUndefined()
})
it('creates a small text-only notification routed to its own app', async () => {
source.capabilities = ['notifications']
const handler = createHandler()
const result = await handler.handle({
method: 'notification.create',
payload: {
sound: 'signal',
subtitle: ' Update ',
text: ' Work completed. ',
title: ' Example ',
},
requestId: 'notification',
})
expect(createNotification).toHaveBeenCalledWith({
appId: 'source-app',
route: '/apps/source-app',
sound: 'signal',
subtitle: 'Update',
text: 'Work completed.',
title: 'Example',
})
expect(result?.response.data).toEqual({
notificationId: 'notification-1',
})
})
it('rejects notification route injection and undeclared notification use', async () => {
source.capabilities = ['notifications']
const handler = createHandler()
const injected = await handler.handle({
method: 'notification.create',
payload: {
route: 'https://example.test',
text: 'Body',
title: 'Title',
},
requestId: 'notification-injected',
})
source.capabilities = []
const denied = await handler.handle({
method: 'notification.create',
payload: { text: 'Body', title: 'Title' },
requestId: 'notification-denied',
})
expect(injected?.response.error).toBe('invalid_notification')
expect(denied?.response.error).toBe('permission_denied')
expect(createNotification).not.toHaveBeenCalled()
})
it('opens only registered targets and passes JSON data to external apps', async () => {
source.capabilities = ['app.open']
resolveTarget.mockReturnValue({
external: true,
id: 'target-app',
route: '/apps/target-app',
})
const handler = createHandler()
const data = { screen: 'details', selectedId: 17 }
const result = await handler.handle({
method: 'app.open',
payload: { appId: 'target-app', data },
requestId: 'open-target',
})
expect(resolveTarget).toHaveBeenCalledWith('target-app')
expect(prepareExternalOpen).toHaveBeenCalledWith('target-app', data)
expect(result).toMatchObject({
effect: { route: '/apps/target-app', type: 'open' },
response: {
data: { appId: 'target-app' },
success: true,
},
})
})
it('rejects foreign URLs, unknown apps, and data for built-in apps', async () => {
source.capabilities = ['app.open']
const handler = createHandler()
const foreignUrl = await handler.handle({
method: 'app.open',
payload: { appId: 'target-app', url: 'https://example.test' },
requestId: 'open-url',
})
const missing = await handler.handle({
method: 'app.open',
payload: { appId: 'missing-app' },
requestId: 'open-missing',
})
resolveTarget.mockReturnValue({
external: false,
id: 'notes',
route: '/apps/notes',
})
const builtInData = await handler.handle({
method: 'app.open',
payload: { appId: 'notes', data: { noteId: 'one' } },
requestId: 'open-built-in-data',
})
expect(foreignUrl?.response.error).toBe('invalid_app_open')
expect(missing?.response.error).toBe('app_not_found')
expect(builtInData?.response.error).toBe('open_data_not_supported')
expect(prepareExternalOpen).not.toHaveBeenCalled()
})
})