import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, test } from 'vitest'; import { formatApiServerLogTimestamp, mergeApiServerEnv, resolveApiServerLogFile, } from './api-server-dev.mjs'; type EnvMap = Record; function withTempEnvFiles( files: Record, assertEnv: (env: EnvMap, tempDir: string) => void, ) { const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-env-')); try { for (const [fileName, content] of Object.entries(files)) { writeFileSync(join(tempDir, fileName), content, 'utf8'); } assertEnv(mergeApiServerEnv(tempDir, {}) as EnvMap, tempDir); } finally { rmSync(tempDir, { recursive: true, force: true }); } } describe('api-server-dev env merge', () => { test('.env.local 和 .env.secrets.local 可以覆盖 .env 默认值', () => { withTempEnvFiles( { '.env': [ 'SMS_AUTH_ENABLED=false', 'HYPER3D_API_KEY=', 'GENARRATIVE_SPACETIME_DATABASE=from-env', ].join('\n'), '.env.local': [ 'SMS_AUTH_ENABLED=true', 'HYPER3D_API_KEY=local-key', ].join('\n'), '.env.secrets.local': 'HYPER3D_API_KEY=secret-key', }, (env) => { expect(env.SMS_AUTH_ENABLED).toBe('true'); expect(env.HYPER3D_API_KEY).toBe('secret-key'); expect(env.GENARRATIVE_SPACETIME_DATABASE).toBe('from-env'); }, ); }); test('外层 shell 变量优先于本地 env 文件', () => { withTempEnvFiles( { '.env': 'HYPER3D_API_KEY=from-env', '.env.local': 'HYPER3D_API_KEY=from-local', '.env.secrets.local': 'HYPER3D_API_KEY=from-secrets', }, (_env, tempDir) => { expect( mergeApiServerEnv(tempDir, { HYPER3D_API_KEY: 'from-shell' }) .HYPER3D_API_KEY, ).toBe('from-shell'); }, ); }); test('空外层 shell 变量不会遮蔽本地私密配置', () => { withTempEnvFiles( { '.env.local': [ 'ALIYUN_OSS_BUCKET=dev-bucket', 'ALIYUN_OSS_ENDPOINT=oss-cn-shanghai.aliyuncs.com', ].join('\n'), '.env.secrets.local': [ 'ALIYUN_OSS_ACCESS_KEY_ID=local-access-key', 'ALIYUN_OSS_ACCESS_KEY_SECRET=local-access-secret', ].join('\n'), }, (_env, tempDir) => { const env = mergeApiServerEnv(tempDir, { ALIYUN_OSS_BUCKET: '', ALIYUN_OSS_ENDPOINT: ' ', ALIYUN_OSS_ACCESS_KEY_ID: 'shell-access-key', ALIYUN_OSS_ACCESS_KEY_SECRET: '', }); expect(env.ALIYUN_OSS_BUCKET).toBe('dev-bucket'); expect(env.ALIYUN_OSS_ENDPOINT).toBe('oss-cn-shanghai.aliyuncs.com'); expect(env.ALIYUN_OSS_ACCESS_KEY_ID).toBe('shell-access-key'); expect(env.ALIYUN_OSS_ACCESS_KEY_SECRET).toBe('local-access-secret'); }, ); }); }); describe('api-server-dev log file resolution', () => { const fixedDate = new Date(2026, 4, 15, 6, 7, 8); test('默认写入 logs/api-server 的时间戳文件', () => { const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-log-')); try { expect(formatApiServerLogTimestamp(fixedDate)).toBe('20260515-060708'); expect(resolveApiServerLogFile(tempDir, {}, fixedDate)).toBe( join(tempDir, 'logs/api-server/api-server-20260515-060708.log'), ); } finally { rmSync(tempDir, { recursive: true, force: true }); } }); test('GENARRATIVE_API_SERVER_LOG_FILE 优先于日志目录默认值', () => { const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-log-')); try { expect( resolveApiServerLogFile( tempDir, { GENARRATIVE_API_SERVER_LOG_DIR: 'logs/ignored', GENARRATIVE_API_SERVER_LOG_FILE: 'logs/custom/api.log', }, fixedDate, ), ).toBe(join(tempDir, 'logs/custom/api.log')); } finally { rmSync(tempDir, { recursive: true, force: true }); } }); });