mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 21:15:56 +08:00
* feat(self-host): add capability-aware setup * fix(self-host): preserve capability compatibility * fix(copilot): honor preview availability server-side * improvement(self-host): centralize capability resolution * fix(self-host): preserve integration availability paths * fix(testing): align capability-aware config mocks * improvement(self-host): simplify capability setup configuration * fix(setup): preserve unowned storage overrides * fix(self-host): reconcile storage and allowlists * fix(integrations): preserve connect deep links
233 lines
7.3 KiB
TypeScript
233 lines
7.3 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
|
import {
|
|
EMAIL_CAPABILITY,
|
|
inspectCapability,
|
|
requireCapability,
|
|
STORAGE_CAPABILITY,
|
|
validateCapabilityFieldInput,
|
|
} from '../../apps/sim/lib/core/config/env-capabilities.ts'
|
|
import { EMAIL_SETUP, STORAGE_SETUP } from './capability-config.ts'
|
|
import {
|
|
buildCapabilitySetupTransition,
|
|
resolveCurrentCapabilitySetupOptionId,
|
|
} from './capability-setup.ts'
|
|
|
|
function applyResult(
|
|
initial: Record<string, string>,
|
|
result: { values: Record<string, string>; remove: readonly string[] }
|
|
): Record<string, string> {
|
|
const reconciled = { ...initial }
|
|
for (const key of result.remove) Reflect.deleteProperty(reconciled, key)
|
|
return Object.assign(reconciled, result.values)
|
|
}
|
|
|
|
describe('setup provider reconciliation', () => {
|
|
it('defaults email setup to the first runtime-ready fallback', () => {
|
|
const vars = new Map([
|
|
['SMTP_HOST', 'smtp.example.com'],
|
|
[
|
|
'GMAIL_CREDENTIALS_JSON',
|
|
JSON.stringify({ client_email: 'service@example.com', private_key: 'secret' }),
|
|
],
|
|
['GMAIL_SENDER', 'sender@example.com'],
|
|
])
|
|
|
|
expect(resolveCurrentCapabilitySetupOptionId(EMAIL_SETUP, vars)).toBe('gmail')
|
|
expect(
|
|
resolveCurrentCapabilitySetupOptionId(
|
|
EMAIL_SETUP,
|
|
new Map([['SMTP_HOST', 'smtp.example.com']])
|
|
)
|
|
).toBe('smtp')
|
|
})
|
|
|
|
it('uses canonical storage selection for setup defaults', () => {
|
|
expect(
|
|
resolveCurrentCapabilitySetupOptionId(STORAGE_SETUP, new Map([['AWS_REGION', 'us-east-1']]))
|
|
).toBe('local')
|
|
expect(
|
|
resolveCurrentCapabilitySetupOptionId(
|
|
STORAGE_SETUP,
|
|
new Map([
|
|
['STORAGE_PROVIDER', ' S3 '],
|
|
['AWS_REGION', 'us-east-1'],
|
|
['S3_BUCKET_NAME', 'files'],
|
|
['S3_ENDPOINT', 'https://storage.example.com'],
|
|
])
|
|
)
|
|
).toBe('s3')
|
|
})
|
|
|
|
it('preserves other ready email providers when another fallback is configured', () => {
|
|
const result = buildCapabilitySetupTransition(
|
|
EMAIL_SETUP,
|
|
'resend',
|
|
{ RESEND_API_KEY: 'new-resend-key' },
|
|
{}
|
|
)
|
|
const reconciled = applyResult(
|
|
{
|
|
SMTP_HOST: 'smtp.example.com',
|
|
SMTP_PORT: '587',
|
|
SMTP_USER: 'old-user',
|
|
SMTP_PASS: 'old-pass',
|
|
},
|
|
result
|
|
)
|
|
|
|
expect(result.remove).not.toEqual(expect.arrayContaining(['SMTP_HOST', 'SMTP_PORT']))
|
|
expect(inspectCapability(EMAIL_CAPABILITY, reconciled).providerIds).toEqual(['resend', 'smtp'])
|
|
})
|
|
|
|
it('clears stale SMTP auth for an unauthenticated relay', () => {
|
|
const result = buildCapabilitySetupTransition(
|
|
EMAIL_SETUP,
|
|
'smtp',
|
|
{ SMTP_HOST: 'localhost', SMTP_PORT: '1025' },
|
|
{}
|
|
)
|
|
const reconciled = applyResult({ SMTP_USER: 'old-user', SMTP_PASS: 'old-pass' }, result)
|
|
|
|
expect(reconciled).not.toHaveProperty('SMTP_USER')
|
|
expect(reconciled).not.toHaveProperty('SMTP_PASS')
|
|
expect(inspectCapability(EMAIL_CAPABILITY, reconciled).providerIds).toEqual(['smtp'])
|
|
})
|
|
|
|
it('clears stale static S3 credentials when IAM is selected', () => {
|
|
const result = buildCapabilitySetupTransition(
|
|
STORAGE_SETUP,
|
|
's3',
|
|
{
|
|
AWS_REGION: 'us-east-1',
|
|
S3_BUCKET_NAME: 'files',
|
|
},
|
|
{}
|
|
)
|
|
const reconciled = applyResult(
|
|
{
|
|
AWS_ACCESS_KEY_ID: 'old-access-key',
|
|
AWS_SECRET_ACCESS_KEY: 'old-secret-key',
|
|
S3_ENDPOINT: 'https://old-endpoint.example.com',
|
|
},
|
|
result
|
|
)
|
|
|
|
expect(reconciled).not.toHaveProperty('AWS_ACCESS_KEY_ID')
|
|
expect(reconciled).not.toHaveProperty('AWS_SECRET_ACCESS_KEY')
|
|
expect(reconciled).not.toHaveProperty('S3_ENDPOINT')
|
|
expect(requireCapability(STORAGE_CAPABILITY, reconciled).providerId).toBe('s3')
|
|
})
|
|
|
|
it('preserves specialized S3 bucket overrides that setup does not prompt for', () => {
|
|
const result = buildCapabilitySetupTransition(
|
|
STORAGE_SETUP,
|
|
's3',
|
|
{
|
|
AWS_REGION: 'us-east-1',
|
|
S3_BUCKET_NAME: 'files',
|
|
},
|
|
{
|
|
S3_KB_BUCKET_NAME: 'knowledge',
|
|
S3_CHAT_BUCKET_NAME: 'chat',
|
|
}
|
|
)
|
|
const reconciled = applyResult(
|
|
{
|
|
S3_KB_BUCKET_NAME: 'knowledge',
|
|
S3_CHAT_BUCKET_NAME: 'chat',
|
|
},
|
|
result
|
|
)
|
|
|
|
expect(result.remove).not.toEqual(
|
|
expect.arrayContaining(['S3_KB_BUCKET_NAME', 'S3_CHAT_BUCKET_NAME'])
|
|
)
|
|
expect(reconciled.S3_KB_BUCKET_NAME).toBe('knowledge')
|
|
expect(reconciled.S3_CHAT_BUCKET_NAME).toBe('chat')
|
|
expect(requireCapability(STORAGE_CAPABILITY, reconciled).providerId).toBe('s3')
|
|
})
|
|
|
|
it('clears specialized S3 bucket overrides when switching to local storage', () => {
|
|
const result = buildCapabilitySetupTransition(
|
|
STORAGE_SETUP,
|
|
'local',
|
|
{},
|
|
{
|
|
AWS_REGION: 'us-east-1',
|
|
S3_BUCKET_NAME: 'files',
|
|
S3_KB_BUCKET_NAME: 'knowledge',
|
|
S3_CHAT_BUCKET_NAME: 'chat',
|
|
}
|
|
)
|
|
const reconciled = applyResult(
|
|
{
|
|
AWS_REGION: 'us-east-1',
|
|
S3_BUCKET_NAME: 'files',
|
|
S3_KB_BUCKET_NAME: 'knowledge',
|
|
S3_CHAT_BUCKET_NAME: 'chat',
|
|
},
|
|
result
|
|
)
|
|
|
|
expect(result.remove).toEqual(
|
|
expect.arrayContaining(['S3_KB_BUCKET_NAME', 'S3_CHAT_BUCKET_NAME'])
|
|
)
|
|
expect(reconciled).not.toHaveProperty('S3_KB_BUCKET_NAME')
|
|
expect(reconciled).not.toHaveProperty('S3_CHAT_BUCKET_NAME')
|
|
expect(requireCapability(STORAGE_CAPABILITY, reconciled).providerId).toBe('local')
|
|
})
|
|
|
|
it('clears stale inline GCS credentials when ADC is selected', () => {
|
|
const result = buildCapabilitySetupTransition(
|
|
STORAGE_SETUP,
|
|
'gcs',
|
|
{ GCS_BUCKET_NAME: 'files' },
|
|
{}
|
|
)
|
|
const reconciled = applyResult(
|
|
{
|
|
GCS_PROJECT_ID: 'old-project',
|
|
GCS_CREDENTIALS_JSON: JSON.stringify({
|
|
client_email: 'old@example.com',
|
|
private_key: 'old-key',
|
|
}),
|
|
},
|
|
result
|
|
)
|
|
|
|
expect(reconciled).not.toHaveProperty('GCS_PROJECT_ID')
|
|
expect(reconciled).not.toHaveProperty('GCS_CREDENTIALS_JSON')
|
|
expect(requireCapability(STORAGE_CAPABILITY, reconciled).providerId).toBe('gcs')
|
|
})
|
|
})
|
|
|
|
describe('setup input validation', () => {
|
|
it('validates SMTP ports with the runtime capability rule', () => {
|
|
const validate = (value: string) =>
|
|
validateCapabilityFieldInput(EMAIL_CAPABILITY, 'SMTP_PORT', value)
|
|
expect(validate('587')).toBeUndefined()
|
|
expect(validate('0')).toContain('between 1 and 65535')
|
|
expect(validate('587.5')).toContain('between 1 and 65535')
|
|
})
|
|
|
|
it('accepts only HTTP(S) S3 endpoints', () => {
|
|
const validate = (value: string) =>
|
|
validateCapabilityFieldInput(STORAGE_CAPABILITY, 'S3_ENDPOINT', value)
|
|
expect(validate('https://account.r2.cloudflarestorage.com')).toBeUndefined()
|
|
expect(validate('http://minio:9000')).toBeUndefined()
|
|
expect(validate('ftp://storage.example.com')).toContain('http:// or https://')
|
|
expect(validate('not-a-url')).toContain('http:// or https://')
|
|
})
|
|
|
|
it('requires complete inline service-account JSON', () => {
|
|
const validate = (value: string) =>
|
|
validateCapabilityFieldInput(EMAIL_CAPABILITY, 'GMAIL_CREDENTIALS_JSON', value)
|
|
expect(
|
|
validate(JSON.stringify({ client_email: 'service@example.com', private_key: 'secret' }))
|
|
).toBeUndefined()
|
|
expect(validate('{"client_email":"service@example.com"}')).toContain(
|
|
'client_email and private_key'
|
|
)
|
|
})
|
|
})
|