import {describe, expect, test} from 'bun:test' import {createCloudAdapterRegistry} from './cloudProxy' import {checkChildCapabilities} from './cloud-spi/childCapabilities' import {isServiceType} from './cloud-spi/serviceCatalog' import type {AutoScalingActionName, CloudProvider, CloudServiceAdapter, CloudServiceType, CertificateActionName, DatabaseActionName, DistributionActionName, FileSystemActionName, HttpApiActionName, KubernetesActionName, MonitoringActionName, ResourceActionName, UserPoolActionName} from './cloud-spi/types' /** * Guards the schema-to-implementation contract for every registered adapter. * * A schema is a promise to the frontend: it decides which controls render and * which are greyed out. Three separate bugs came from breaking that promise — * networking advertised create/delete as available while the adapter threw, * serverless shipped an invoke panel against an adapter with no invoke, and the * Azure serverless adapter was advertised as available against a runtime that * answers 501. These tests make the contract mechanical instead of a convention. */ const CLOUDS: CloudProvider[] = ['aws', 'azure', 'gcp'] /** Adapter method that must exist for a capability to claim `available`. */ const METHOD_FOR_ACTION: Record = { list: 'list', create: 'create', update: 'update', delete: 'delete', inspect: 'get', encrypt: 'encrypt', decrypt: 'decrypt', invoke: 'invoke', start: 'start', stop: 'stop', reboot: 'reboot', updateTags: 'updateTags', } const METHOD_FOR_DATABASE_ACTION = { listSnapshots: 'listDatabaseSnapshots', createSnapshot: 'createDatabaseSnapshot', } satisfies Record const METHOD_FOR_KUBERNETES_ACTION: Record = { listNodegroups: 'listKubernetesNodegroups', createNodegroup: 'createKubernetesNodegroup', deleteNodegroup: 'deleteKubernetesNodegroup', listFargateProfiles: 'listKubernetesFargateProfiles', createFargateProfile: 'createKubernetesFargateProfile', deleteFargateProfile: 'deleteKubernetesFargateProfile', } const METHOD_FOR_MONITORING_ACTION: Record = { listMetrics: 'listMetrics', getMetricData: 'getMetricSeries', alarmHistory: 'describeAlarmHistory', setAlarmState: 'setAlarmState', setAlarmActions: 'setAlarmActionsEnabled', } const METHOD_FOR_HTTP_API_ACTION: Record = { describe: 'describeHttpApi', createRoute: 'createHttpApiRoute', deleteRoute: 'deleteHttpApiRoute', setRouteAuthorizer: 'setHttpApiRouteAuthorizer', createStage: 'createHttpApiStage', deleteStage: 'deleteHttpApiStage', deploy: 'deployHttpApi', createAuthorizer: 'createHttpApiAuthorizer', deleteAuthorizer: 'deleteHttpApiAuthorizer', updateCors: 'updateHttpApiCors', sendRequest: 'testApiGatewayRoute', testWebSocket: 'testWebSocketApi', } const METHOD_FOR_DISTRIBUTION_ACTION: Record = { setEnabled: 'setDistributionEnabled', createInvalidation: 'createDistributionInvalidation', } const METHOD_FOR_CERTIFICATE_ACTION: Record = { download: 'getCertificatePem', export: 'exportCertificate', revoke: 'revokeCertificate', } const METHOD_FOR_FILE_SYSTEM_ACTION: Record = { createMountTarget: 'createMountTarget', deleteMountTarget: 'deleteMountTarget', createAccessPoint: 'createAccessPoint', deleteAccessPoint: 'deleteAccessPoint', attachFunction: 'attachFileSystemToFunction', detachFunction: 'detachFileSystemFromFunction', } const METHOD_FOR_AUTO_SCALING_ACTION: Record = { setCapacity: 'setAutoScalingCapacity', terminateInstance: 'terminateAutoScalingInstance', } const METHOD_FOR_USER_POOL_ACTION: Record = { updateUser: 'updateUserPoolUser', setUserGroups: 'setUserPoolUserGroups', listGroups: 'listUserPoolGroups', createGroup: 'createUserPoolGroup', deleteGroup: 'deleteUserPoolGroup', testSignIn: 'testUserPoolSignIn', describeClient: 'describeUserPoolClient', updateClientOAuth: 'updateUserPoolClientOAuth', } const registry = createCloudAdapterRegistry() function registeredAdapters(): Array<{cloud: CloudProvider; adapter: CloudServiceAdapter}> { return CLOUDS.flatMap((cloud) => registry.servicesFor(cloud).map((service) => ({cloud, adapter: adapterFor(cloud, service)})), ) } function adapterFor(cloud: CloudProvider, service: CloudServiceType): CloudServiceAdapter { const adapter = registry.get(cloud, service) if (!adapter) throw new Error(`expected an adapter for ${cloud}/${service}`) return adapter } describe('registered adapters honour their schema', () => { const adapters = registeredAdapters() test('at least one adapter is registered for every cloud', () => { for (const cloud of CLOUDS) { expect(adapters.filter((entry) => entry.cloud === cloud).length).toBeGreaterThan(0) } }) for (const {cloud, adapter} of adapters) { const label = `${cloud}/${adapter.service}` test(`${label} schema identifies itself consistently`, () => { const schema = adapter.schema() expect(schema.cloud).toBe(adapter.cloud) expect(schema.service).toBe(adapter.service) expect(isServiceType(schema.service)).toBe(true) expect(schema.displayName.length).toBeGreaterThan(0) }) test(`${label} implements every action it advertises`, () => { for (const action of adapter.schema().actions) { const method = METHOD_FOR_ACTION[action] expect(typeof adapter[method]).toBe('function') } }) test(`${label} implements every capability it marks available`, () => { const capabilities = adapter.schema().capabilities?.resourceActions ?? [] const claimed = capabilities.filter((capability) => capability.status === 'available') for (const capability of claimed) { const method = METHOD_FOR_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as available but has no ${String(method)}()`, ).toBe('function') } const databaseCapabilities = adapter.schema().capabilities?.databaseActions ?? [] const databaseClaims = databaseCapabilities.filter( (capability) => capability.status === 'available' || (capability.status === 'partial' && capability.enabled), ) for (const capability of databaseClaims) { const method = METHOD_FOR_DATABASE_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as ${capability.status} but has no ${String(method)}()`, ).toBe('function') } const kubernetesCapabilities = adapter.schema().capabilities?.kubernetesActions ?? [] const kubernetesClaims = kubernetesCapabilities.filter((capability) => capability.status === 'available') for (const capability of kubernetesClaims) { const method = METHOD_FOR_KUBERNETES_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as available but has no ${String(method)}()`, ).toBe('function') } const userPoolClaims = (adapter.schema().capabilities?.userPoolActions ?? []).filter((capability) => capability.status === 'available') for (const capability of userPoolClaims) { const method = METHOD_FOR_USER_POOL_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as available but has no ${String(method)}()`, ).toBe('function') } const monitoringClaims = (adapter.schema().capabilities?.monitoringActions ?? []).filter((capability) => capability.status === 'available') for (const capability of monitoringClaims) { const method = METHOD_FOR_MONITORING_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as available but has no ${String(method)}()`, ).toBe('function') } const httpApiClaims = (adapter.schema().capabilities?.httpApiActions ?? []).filter((capability) => capability.status === 'available') for (const capability of httpApiClaims) { const method = METHOD_FOR_HTTP_API_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as available but has no ${String(method)}()`, ).toBe('function') } const certificateClaims = (adapter.schema().capabilities?.certificateActions ?? []).filter((capability) => capability.status === 'available') for (const capability of certificateClaims) { const method = METHOD_FOR_CERTIFICATE_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as available but has no ${String(method)}()`, ).toBe('function') } const autoScalingClaims = (adapter.schema().capabilities?.autoScalingActions ?? []).filter((capability) => capability.status === 'available') for (const capability of autoScalingClaims) { const method = METHOD_FOR_AUTO_SCALING_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as available but has no ${String(method)}()`, ).toBe('function') } const fileSystemClaims = (adapter.schema().capabilities?.fileSystemActions ?? []).filter((capability) => capability.status === 'available') for (const capability of fileSystemClaims) { const method = METHOD_FOR_FILE_SYSTEM_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as available but has no ${String(method)}()`, ).toBe('function') } const distributionClaims = (adapter.schema().capabilities?.distributionActions ?? []).filter((capability) => capability.status === 'available') for (const capability of distributionClaims) { const method = METHOD_FOR_DISTRIBUTION_ACTION[capability.name] expect( typeof adapter[method], `${label} advertises ${capability.name} as available but has no ${String(method)}()`, ).toBe('function') } }) test(`${label} child collections match their advertised capabilities`, () => { expect(checkChildCapabilities(adapter), `${label} child capability mismatch`).toEqual([]) }) test(`${label} explains every capability it does not fully support`, () => { const capabilities = [ ...(adapter.schema().capabilities?.resourceActions ?? []), ...(adapter.schema().capabilities?.objectActions ?? []), ...(adapter.schema().capabilities?.databaseActions ?? []), ...(adapter.schema().capabilities?.kubernetesActions ?? []), ...(adapter.schema().capabilities?.userPoolActions ?? []), ...(adapter.schema().capabilities?.distributionActions ?? []), ...(adapter.schema().capabilities?.autoScalingActions ?? []), ...(adapter.schema().capabilities?.fileSystemActions ?? []), ...(adapter.schema().capabilities?.certificateActions ?? []), ...(adapter.schema().capabilities?.monitoringActions ?? []), ...(adapter.schema().capabilities?.httpApiActions ?? []), ...(adapter.schema().capabilities?.collectionActions ?? []), ...(adapter.schema().capabilities?.itemActions ?? []), ] for (const capability of capabilities) { if (capability.status === 'available') continue expect( capability.reason?.length ?? 0, `${label} capability ${capability.name} is ${capability.status} with no reason`, ).toBeGreaterThan(0) } }) test(`${label} declares object capabilities only when it can serve them`, () => { const objectActions = adapter.schema().capabilities?.objectActions ?? [] if (objectActions.length === 0) return // An object-action block means the resource is a container of objects. expect(typeof adapter.listObjects).toBe('function') }) test(`${label} columns resolve to a path`, () => { for (const column of adapter.schema().columns) { expect((column.path ?? column.name).length).toBeGreaterThan(0) } }) } }) describe('adapter capability advertisements match the runtime reality', () => { test('AWS database advertises instance and snapshot capabilities', () => { const schema = adapterFor('aws', 'database').schema() expect(schema.actions).toEqual(['list', 'create', 'update', 'delete', 'inspect']) expect(schema.capabilities?.resourceActions?.map(({name, enabled, status, runtimeRequired}) => ({ name, enabled, status, runtimeRequired, }))).toEqual([ {name: 'list', enabled: true, status: 'available', runtimeRequired: true}, {name: 'create', enabled: true, status: 'available', runtimeRequired: true}, {name: 'update', enabled: true, status: 'available', runtimeRequired: true}, {name: 'delete', enabled: true, status: 'available', runtimeRequired: true}, {name: 'inspect', enabled: true, status: 'available', runtimeRequired: true}, {name: 'start', enabled: true, status: 'available', runtimeRequired: true}, {name: 'stop', enabled: true, status: 'available', runtimeRequired: true}, {name: 'reboot', enabled: true, status: 'available', runtimeRequired: true}, ]) expect(schema.capabilities?.resourceActions?.find(({name}) => name === 'create')?.label).toBe('Create DB instance') expect(schema.capabilities?.resourceActions?.find(({name}) => name === 'update')?.label).toBe('Update DB instance') expect(schema.capabilities?.databaseActions).toEqual([ { name: 'listSnapshots', label: 'List DB snapshots', enabled: true, status: 'available', runtimeRequired: true, }, { name: 'createSnapshot', label: 'Create DB snapshot', enabled: true, status: 'partial', reason: 'The Cloud Proxy operation is available, but the current runtime does not implement CreateDBSnapshot.', runtimeRequired: true, }, ]) }) test('AWS serverless advertises invoke and implements it', () => { const adapter = adapterFor('aws', 'serverless') const invoke = adapter.schema().capabilities?.resourceActions?.find((c) => c.name === 'invoke') expect(invoke?.status).toBe('available') expect(typeof adapter.invoke).toBe('function') }) test('AWS networking does not advertise create or delete as available', () => { const adapter = adapterFor('aws', 'networking') const actions = adapter.schema().capabilities?.resourceActions ?? [] for (const name of ['create', 'delete'] as const) { const capability = actions.find((c) => c.name === name) expect(capability?.status).not.toBe('available') expect(capability?.reason).toBeTruthy() } }) test('Azure serverless reports its runtime gap through descriptorOverride', () => { const override = adapterFor('azure', 'serverless').descriptorOverride?.() expect(override?.availability).toBe('coming_soon') expect(override?.reason).toContain('501') }) })