Files
simstudioai--sim/scripts/setup/configuration-sources.test.ts
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

628 lines
24 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'bun:test'
import {
type ConfigurationCommandRunner,
type ConfigurationSourceDiscoveryOptions,
discoverConfigurationSources as discoverConfigurationSourcesFromEnvironment,
parseComposeFileEnvironment,
resolveKubernetesContainerEnvironment,
} from './configuration-sources.ts'
const temporaryDirectories: string[] = []
function temporaryDirectory(): string {
const directory = mkdtempSync(path.join(tmpdir(), 'sim-configuration-sources-'))
temporaryDirectories.push(directory)
return directory
}
function commandResult(status: number, stdout = '') {
return { status, stdout, stderr: '' }
}
function discoverConfigurationSources(options: ConfigurationSourceDiscoveryOptions) {
return discoverConfigurationSourcesFromEnvironment({ processEnvironment: {}, ...options })
}
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
describe('discoverConfigurationSources', () => {
it('enumerates split development and prepared Compose configuration separately', () => {
const root = temporaryDirectory()
mkdirSync(path.join(root, 'apps/sim'), { recursive: true })
mkdirSync(path.join(root, 'apps/realtime'), { recursive: true })
mkdirSync(path.join(root, 'packages/db'), { recursive: true })
writeFileSync(path.join(root, 'apps/sim/.env'), 'RESEND_API_KEY=dev-email\n')
writeFileSync(path.join(root, 'apps/realtime/.env'), 'REDIS_URL=redis://localhost:6379\n')
writeFileSync(path.join(root, 'packages/db/.env'), 'DATABASE_URL=postgresql://dev\n')
writeFileSync(path.join(root, '.env'), 'RESEND_API_KEY=compose-email\n')
writeFileSync(
path.join(root, 'docker-compose.prod.yml'),
`services:
simstudio:
image: ghcr.io/simstudioai/simstudio:latest
env_file:
- .env
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
- NEXT_PUBLIC_APP_URL=\${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
- BETTER_AUTH_URL=\${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
- REDIS_URL=\${REDIS_URL:-redis://redis:6379}
`
)
const runner: ConfigurationCommandRunner = () => commandResult(1)
const sources = discoverConfigurationSources({ root, runner })
expect(sources).toHaveLength(2)
expect(sources[0]).toMatchObject({ kind: 'dev', managedByCurrentCheckout: true })
expect(sources[0].values?.get('RESEND_API_KEY')).toBe('dev-email')
expect(sources[0].values?.has('DATABASE_URL')).toBe(false)
expect(sources[1]).toMatchObject({ kind: 'compose', managedByCurrentCheckout: false })
expect(sources[1].values?.get('RESEND_API_KEY')).toBe('compose-email')
expect(sources[1].values?.get('REDIS_URL')).toBe('redis://redis:6379')
expect(sources[1].values?.get('DATABASE_URL')).toBe(
'postgresql://postgres:postgres@db:5432/simstudio'
)
})
it('uses the last active value from a prepared Compose .env file', () => {
const root = temporaryDirectory()
writeFileSync(path.join(root, '.env'), 'RESEND_API_KEY=old\nRESEND_API_KEY=current\n')
writeFileSync(
path.join(root, 'docker-compose.prod.yml'),
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n env_file: .env\n'
)
const sources = discoverConfigurationSources({ root, runner: () => commandResult(1) })
expect(sources).toHaveLength(1)
expect(sources[0].values?.get('RESEND_API_KEY')).toBe('current')
})
it('uses the effective environment of a stopped Compose app container', () => {
const parent = temporaryDirectory()
const root = path.join(parent, 'checkout')
const deployment = path.join(parent, 'deployment')
mkdirSync(root)
mkdirSync(deployment)
const composeFile = path.join(deployment, 'compose.yml')
writeFileSync(
composeFile,
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n'
)
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'docker' && args[0] === 'info') return commandResult(0)
if (command === 'docker' && args[0] === 'compose' && args[1] === 'ls') {
return commandResult(
0,
JSON.stringify([{ Name: 'external-sim', Status: 'exited', ConfigFiles: composeFile }])
)
}
if (command === 'docker' && args[0] === 'ps') return commandResult(0, 'container-id\n')
if (command === 'docker' && args[0] === 'inspect') {
return commandResult(
0,
JSON.stringify([
{
Created: '2026-01-01T00:00:00Z',
State: { Running: false },
Config: { Env: ['RESEND_API_KEY=secret', 'REDIS_URL=redis://redis:6379'] },
},
])
)
}
return commandResult(1)
}
const sources = discoverConfigurationSources({ root, runner })
expect(sources).toHaveLength(1)
expect(sources[0]).toMatchObject({
kind: 'compose',
label: 'Compose project "external-sim"',
managedByCurrentCheckout: false,
})
expect(sources[0].values?.get('REDIS_URL')).toBe('redis://redis:6379')
expect(sources[0].values?.get('RESEND_API_KEY')).toBe('secret')
})
it('replaces the prepared root source with one unambiguous live project', () => {
const root = temporaryDirectory()
writeFileSync(path.join(root, '.env'), 'RESEND_API_KEY=prepared\n')
const composeFile = path.join(root, 'docker-compose.prod.yml')
writeFileSync(
composeFile,
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n'
)
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'docker' && args[0] === 'info') return commandResult(0)
if (command === 'docker' && args[0] === 'compose' && args[1] === 'ls') {
return commandResult(
0,
JSON.stringify([{ Name: 'current-sim', Status: 'running', ConfigFiles: composeFile }])
)
}
if (command === 'docker' && args[0] === 'ps') return commandResult(0, 'container-id\n')
if (command === 'docker' && args[0] === 'inspect') {
return commandResult(
0,
JSON.stringify([
{
Created: '2026-01-01T00:00:00Z',
State: { Running: true },
Config: { Env: ['RESEND_API_KEY=effective'] },
},
])
)
}
return commandResult(1)
}
const sources = discoverConfigurationSources({ root, runner })
expect(sources).toHaveLength(1)
expect(sources[0]).toMatchObject({
label: 'Compose project "current-sim"',
managedByCurrentCheckout: true,
})
expect(sources[0].values?.get('RESEND_API_KEY')).toBe('effective')
})
it('does not claim a live current-checkout project is setup-managed without root .env', () => {
const root = temporaryDirectory()
const composeFile = path.join(root, 'docker-compose.prod.yml')
writeFileSync(
composeFile,
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n'
)
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'docker' && args[0] === 'info') return commandResult(0)
if (command === 'docker' && args[0] === 'compose' && args[1] === 'ls') {
return commandResult(
0,
JSON.stringify([{ Name: 'current-sim', Status: 'running', ConfigFiles: composeFile }])
)
}
if (command === 'docker' && args[0] === 'ps') return commandResult(0, 'container-id\n')
if (command === 'docker' && args[0] === 'inspect') {
return commandResult(
0,
JSON.stringify([{ State: { Running: true }, Config: { Env: ['NODE_ENV=production'] } }])
)
}
return commandResult(1)
}
const sources = discoverConfigurationSources({ root, runner })
expect(sources).toHaveLength(1)
expect(sources[0].managedByCurrentCheckout).toBe(false)
})
it('loads development env files with Next precedence', () => {
const root = temporaryDirectory()
const appDirectory = path.join(root, 'apps/sim')
mkdirSync(appDirectory, { recursive: true })
writeFileSync(path.join(appDirectory, '.env'), 'STATUS_PRECEDENCE=base\n')
writeFileSync(path.join(appDirectory, '.env.development'), 'STATUS_PRECEDENCE=development\n')
writeFileSync(path.join(appDirectory, '.env.local'), 'STATUS_PRECEDENCE=local\n')
writeFileSync(
path.join(appDirectory, '.env.development.local'),
'STATUS_PRECEDENCE=development-local\n'
)
const sources = discoverConfigurationSources({ root, runner: () => commandResult(1) })
expect(sources).toHaveLength(1)
expect(sources[0].values?.get('STATUS_PRECEDENCE')).toBe('development-local')
expect(sources[0].location).toContain('.env.development.local')
expect(sources[0].location).toContain('process environment')
expect(sources[0].managedByCurrentCheckout).toBe(false)
})
it('does not offer setup writes when a process-only capability value wins', () => {
const root = temporaryDirectory()
const appDirectory = path.join(root, 'apps/sim')
mkdirSync(appDirectory, { recursive: true })
writeFileSync(path.join(appDirectory, '.env'), 'SLACK_CLIENT_SECRET=file-secret\n')
const sources = discoverConfigurationSources({
root,
runner: () => commandResult(1),
processEnvironment: { SLACK_CLIENT_ID: 'process-client-id' },
})
expect(sources[0].values?.get('SLACK_CLIENT_ID')).toBe('process-client-id')
expect(sources[0].managedByCurrentCheckout).toBe(false)
})
it('reports missing files in the split development configuration', () => {
const root = temporaryDirectory()
const appDirectory = path.join(root, 'apps/sim')
mkdirSync(appDirectory, { recursive: true })
writeFileSync(
path.join(appDirectory, '.env'),
[
'DATABASE_URL=postgresql://localhost/sim',
`BETTER_AUTH_SECRET=${'a'.repeat(32)}`,
'BETTER_AUTH_URL=http://localhost:3000',
'NEXT_PUBLIC_APP_URL=http://localhost:3000',
`ENCRYPTION_KEY=${'b'.repeat(64)}`,
`INTERNAL_API_SECRET=${'c'.repeat(32)}`,
].join('\n')
)
const sources = discoverConfigurationSources({ root, runner: () => commandResult(1) })
expect(sources).toHaveLength(1)
expect(sources[0].configurationIssues).toEqual(
expect.arrayContaining(['apps/realtime/.env is missing', 'packages/db/.env is missing'])
)
})
it('reports shared-value drift across split development env files', () => {
const root = temporaryDirectory()
mkdirSync(path.join(root, 'apps/sim'), { recursive: true })
mkdirSync(path.join(root, 'apps/realtime'), { recursive: true })
mkdirSync(path.join(root, 'packages/db'), { recursive: true })
writeFileSync(
path.join(root, 'apps/sim/.env'),
[
'DATABASE_URL=postgresql://localhost/sim',
`BETTER_AUTH_SECRET=${'a'.repeat(32)}`,
'BETTER_AUTH_URL=http://localhost:3000',
'NEXT_PUBLIC_APP_URL=http://localhost:3000',
`ENCRYPTION_KEY=${'b'.repeat(64)}`,
`INTERNAL_API_SECRET=${'c'.repeat(32)}`,
].join('\n')
)
writeFileSync(
path.join(root, 'apps/realtime/.env'),
[
'DATABASE_URL=postgresql://localhost/sim',
`BETTER_AUTH_SECRET=${'d'.repeat(32)}`,
'BETTER_AUTH_URL=http://localhost:3000',
'NEXT_PUBLIC_APP_URL=http://localhost:3000',
`INTERNAL_API_SECRET=${'c'.repeat(32)}`,
].join('\n')
)
writeFileSync(path.join(root, 'packages/db/.env'), 'DATABASE_URL=postgresql://localhost/other')
const sources = discoverConfigurationSources({ root, runner: () => commandResult(1) })
expect(sources[0].configurationIssues).toEqual(
expect.arrayContaining([
'BETTER_AUTH_SECRET differs between apps/sim/.env and apps/realtime/.env',
'DATABASE_URL differs between apps/sim/.env and packages/db/.env',
])
)
})
it('identifies a Helm release by current context, namespace, and release', () => {
const root = temporaryDirectory()
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'helm') {
if (args.includes('-a') || !args.includes('--deployed') || !args.includes('--failed')) {
return commandResult(1)
}
if (args.at(-2) !== '--kube-context' || args.at(-1) !== 'production-cluster') {
return commandResult(1)
}
return commandResult(
0,
JSON.stringify([{ name: 'sim-prod', namespace: 'production', chart: 'sim-1.2.3' }])
)
}
if (command === 'kubectl' && args[0] === 'config') {
return commandResult(0, 'production-cluster\n')
}
if (!args.includes('--context') || !args.includes('production-cluster')) {
return commandResult(1)
}
if (command === 'kubectl' && args[1] === 'deployments') {
return commandResult(
0,
JSON.stringify({
items: [
{
spec: {
template: {
spec: {
containers: [
{
name: 'app',
env: [{ name: 'NEXT_PUBLIC_APP_URL', value: 'https://sim.example.com' }],
},
],
},
},
},
},
],
})
)
}
return commandResult(1)
}
const sources = discoverConfigurationSources({ root, runner })
expect(sources).toHaveLength(1)
expect(sources[0]).toMatchObject({
kind: 'helm',
label: 'Helm release "sim-prod"',
location: 'context production-cluster · namespace production · release sim-prod',
managedByCurrentCheckout: false,
})
expect(sources[0].values?.get('NEXT_PUBLIC_APP_URL')).toBe('https://sim.example.com')
})
it('keeps every Compose config file after one file identifies Sim', () => {
const parent = temporaryDirectory()
const root = path.join(parent, 'checkout')
const deployment = path.join(parent, 'deployment')
mkdirSync(root)
mkdirSync(deployment)
const baseFile = path.join(deployment, 'compose.yml')
const overrideFile = path.join(deployment, 'compose.override.yml')
writeFileSync(
baseFile,
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n'
)
writeFileSync(
overrideFile,
'services:\n simstudio:\n environment:\n - REDIS_URL=redis://override\n'
)
let renderedWithOverride = false
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'docker' && args[0] === 'info') return commandResult(0)
if (command === 'docker' && args[0] === 'compose' && args[1] === 'ls') {
return commandResult(
0,
JSON.stringify([
{
Name: 'external-sim',
Status: 'running',
ConfigFiles: `${baseFile},${overrideFile}`,
},
])
)
}
if (command === 'docker' && args[0] === 'ps') return commandResult(0)
if (command === 'docker' && args[0] === 'compose' && args.includes('config')) {
renderedWithOverride = args.includes(overrideFile)
return commandResult(
0,
JSON.stringify({
services: { simstudio: { environment: { REDIS_URL: 'redis://override' } } },
})
)
}
return commandResult(1)
}
const sources = discoverConfigurationSources({ root, runner })
expect(renderedWithOverride).toBe(true)
expect(sources[0].location).toContain(overrideFile)
expect(sources[0].values?.get('REDIS_URL')).toBe('redis://override')
})
it('fails fast when Docker Compose returns malformed discovery output', () => {
const root = temporaryDirectory()
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'docker' && args[0] === 'info') return commandResult(0)
if (command === 'docker' && args[0] === 'compose' && args[1] === 'ls') {
return commandResult(0, 'not-json')
}
return commandResult(1)
}
expect(() => discoverConfigurationSources({ root, runner })).toThrow(
'Docker Compose returned an invalid project list'
)
})
it('fails fast when a Docker Compose project entry changes shape', () => {
const root = temporaryDirectory()
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'docker' && args[0] === 'info') return commandResult(0)
if (command === 'docker' && args[0] === 'compose' && args[1] === 'ls') {
return commandResult(0, JSON.stringify([{ Name: 'sim-without-config-files' }]))
}
return commandResult(1)
}
expect(() => discoverConfigurationSources({ root, runner })).toThrow(
'Docker Compose returned an invalid project list'
)
})
it('does not treat unrelated Docker tooling as a Sim configuration source', () => {
const root = temporaryDirectory()
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'docker' && args[0] === '--version') return commandResult(0)
return commandResult(1)
}
const sources = discoverConfigurationSources({ root, runner })
expect(sources).toHaveLength(0)
})
it('does not treat an unrelated Kubernetes context as a Sim configuration source', () => {
const root = temporaryDirectory()
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'kubectl' && args[0] === 'config') {
return commandResult(0, 'production-cluster\n')
}
return commandResult(1)
}
const sources = discoverConfigurationSources({ root, runner })
expect(sources).toHaveLength(0)
})
it('does not substitute desired Compose values when a known container cannot be inspected', () => {
const root = temporaryDirectory()
const composeFile = path.join(root, 'docker-compose.prod.yml')
writeFileSync(
composeFile,
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n'
)
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'docker' && args[0] === 'info') return commandResult(0)
if (command === 'docker' && args[0] === 'compose' && args[1] === 'ls') {
return commandResult(
0,
JSON.stringify([{ Name: 'known-sim', Status: 'running', ConfigFiles: composeFile }])
)
}
if (command === 'docker' && args[0] === 'ps') return commandResult(0, 'container-id\n')
if (command === 'docker' && args[0] === 'inspect') return commandResult(1)
if (command === 'docker' && args[0] === 'compose') {
return commandResult(
0,
JSON.stringify({
services: { simstudio: { environment: { RESEND_API_KEY: 'desired' } } },
})
)
}
return commandResult(1)
}
const sources = discoverConfigurationSources({ root, runner })
expect(sources).toHaveLength(1)
expect(sources[0].values).toBeNull()
expect(sources[0].warning).toContain('could not be inspected')
})
it('reports a known Compose project as unknown when its containers cannot be enumerated', () => {
const root = temporaryDirectory()
const composeFile = path.join(root, 'docker-compose.prod.yml')
writeFileSync(
composeFile,
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n'
)
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'docker' && args[0] === 'info') return commandResult(0)
if (command === 'docker' && args[0] === 'compose' && args[1] === 'ls') {
return commandResult(
0,
JSON.stringify([{ Name: 'known-sim', Status: 'running', ConfigFiles: composeFile }])
)
}
if (command === 'docker' && args[0] === 'ps') return commandResult(1)
return commandResult(1)
}
const sources = discoverConfigurationSources({ root, runner })
expect(sources).toHaveLength(1)
expect(sources[0].values).toBeNull()
expect(sources[0].warning).toContain('could not be enumerated')
})
it('fails fast when a Helm release entry changes shape', () => {
const root = temporaryDirectory()
const runner: ConfigurationCommandRunner = (command, args) => {
if (command === 'kubectl' && args[0] === 'config') {
return commandResult(0, 'production-cluster\n')
}
if (command === 'helm' && args[0] === 'list') {
return commandResult(0, JSON.stringify([{ name: 'sim-prod', namespace: 'production' }]))
}
return commandResult(1)
}
expect(() => discoverConfigurationSources({ root, runner })).toThrow(
'Helm returned an invalid release list'
)
})
})
describe('parseComposeFileEnvironment', () => {
it('does not inject root .env when the service has no env_file', () => {
const values = parseComposeFileEnvironment(
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n environment:\n - REDIS_URL=redis://redis:6379\n',
new Map([['RESEND_API_KEY', 'must-not-be-injected']])
)
expect(values?.get('REDIS_URL')).toBe('redis://redis:6379')
expect(values?.has('RESEND_API_KEY')).toBe(false)
})
it('returns unknown for unsupported inline environment syntax', () => {
const values = parseComposeFileEnvironment(
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n env_file: .env\n environment: { RESEND_API_KEY: override }\n',
new Map([['RESEND_API_KEY', 'root-value']])
)
expect(values).toBeNull()
})
})
describe('resolveKubernetesContainerEnvironment', () => {
it('applies envFrom order and then explicit env/valueFrom precedence', () => {
const resources = new Map([
[
'secret/app-secret',
{
data: {
SHARED: Buffer.from('secret').toString('base64'),
SECRET_ONLY: Buffer.from('secret-only').toString('base64'),
EXPLICIT_SOURCE: Buffer.from('from-secret-key').toString('base64'),
},
},
],
['configmap/app-config', { data: { SHARED: 'configmap', CONFIG_ONLY: 'config-only' } }],
])
const resolution = resolveKubernetesContainerEnvironment(
{
envFrom: [{ secretRef: { name: 'app-secret' } }, { configMapRef: { name: 'app-config' } }],
env: [
{ name: 'SHARED', value: 'explicit' },
{
name: 'FROM_SECRET',
valueFrom: { secretKeyRef: { name: 'app-secret', key: 'EXPLICIT_SOURCE' } },
},
],
},
(kind, name) => {
const resource = resources.get(`${kind}/${name}`)
return resource ? { state: 'found', resource } : { state: 'missing' }
}
)
expect(resolution.warning).toBeUndefined()
expect(resolution.values).toEqual(
new Map([
['SHARED', 'explicit'],
['SECRET_ONLY', 'secret-only'],
['EXPLICIT_SOURCE', 'from-secret-key'],
['CONFIG_ONLY', 'config-only'],
['FROM_SECRET', 'from-secret-key'],
])
)
})
it('returns unknown instead of claiming missing configuration when a Secret is inaccessible', () => {
const resolution = resolveKubernetesContainerEnvironment(
{ envFrom: [{ secretRef: { name: 'restricted-secret' } }] },
() => ({ state: 'inaccessible' })
)
expect(resolution.values).toBeNull()
expect(resolution.warning).toContain('RBAC')
expect(resolution.warning).not.toContain('secret-value')
})
})