import assert from 'node:assert/strict' import { existsSync, readFileSync } from 'node:fs' import { spawnSync } from 'node:child_process' import test from 'node:test' import { runInNewContext } from 'node:vm' import { createHash } from 'node:crypto' import { aiModelRegistryEvidence } from './fixtures/ai-model-registry-evidence.mjs' const bash = process.platform === 'win32' && existsSync('C:/Program Files/Git/bin/bash.exe') ? 'C:/Program Files/Git/bin/bash.exe' : 'bash' import { extractRemoteTemplate, parseArguments, renderRemoteTemplate, } from './git-incremental-deploy-macos.mjs' test('generated remote deployment payload is valid bash', () => { const source = readFileSync(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8') const payload = extractRemoteTemplate(source).replace(/__[A-Z0-9_]+__/g, "''") const result = spawnSync(bash, ['-n'], { input: payload, encoding: 'utf8', }) assert.equal(result.status, 0, result.stderr || result.stdout) }) test('Mac entry renders every canonical remote deployment token', () => { const source = readFileSync(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8') const template = extractRemoteTemplate(source) const tokens = [...new Set(template.match(/__[A-Z0-9_]+__/g) ?? [])] const values = Object.fromEntries(tokens.map((token, index) => [token, token === '__COMMIT__' ? 'a'.repeat(40) : `value ${index} with quote ' safely escaped`])) const payload = renderRemoteTemplate(template, values) assert.doesNotMatch(payload, /__[A-Z0-9_]+__/) const result = spawnSync(bash, ['-n'], { input: payload, encoding: 'utf8' }) assert.equal(result.status, 0, result.stderr || result.stdout) }) test('Mac entry requires an exact commit outside DryRun', () => { assert.throws(() => parseArguments([]), /正式部署必须/) assert.throws(() => parseArguments(['--commit', 'abc']), /40 位小写/) assert.equal(parseArguments(['--dry-run']).dryRun, true) assert.equal(parseArguments(['--commit', 'b'.repeat(40)]).commit, 'b'.repeat(40)) assert.equal(parseArguments(['--commit', 'b'.repeat(40), '--skip-deploy']).skipDeploy, true) assert.throws( () => parseArguments(['--commit', 'b'.repeat(40), '--reuse-verified-image', '--skip-deploy']), /不能与 --skip-deploy/, ) }) test('deployment SSH hops keep long builds and cutovers alive', () => { const macSource = readFileSync(new URL('./git-incremental-deploy-macos.mjs', import.meta.url), 'utf8') const powerShellSource = readFileSync(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8') const payload = extractRemoteTemplate(powerShellSource) for (const source of [macSource, payload]) { assert.match(source, /ServerAliveInterval=3/) assert.match(source, /ServerAliveCountMax=10/) assert.match(source, /TCPKeepAlive=yes/) } }) test('migration deployment gate recognizes the latest approved schema migration', () => { const source = readFileSync(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8') const gateStart = source.indexOf('const expectedMigrationNames = new Set([') const gateEnd = source.indexOf(']);', gateStart) assert.ok(gateStart >= 0 && gateEnd > gateStart, 'migration gate list is missing') const gate = source.slice(gateStart, gateEnd) assert.match(gate, /20261008_permission_group_department_identity/) assert.match(gate, /20261008_team_function_configs/) assert.match(gate, /20260831_add_private_task_detail/) assert.match(gate, /20260908_add_brand_knowledge/) assert.match(gate, /20260914_add_brand_knowledge_engagement/) assert.match(gate, /20260914_expand_brand_knowledge_download_count/) assert.match(gate, /20260914_expand_brand_knowledge_preview_slices/) assert.match(gate, /20260915_add_brand_knowledge_soft_delete/) assert.match(gate, /20260918_add_performance_conversion_sync/) assert.match(gate, /20261012_detail_generator/) assert.match(gate, /20261012_ai_model_registry/) assert.match(gate, /20261013_ai_model_capabilities/) }) test('direct replacement runs database and dependency gates before switching traffic', () => { const source = readFileSync(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8') .replace(/\r\n/g, '\n') const functionStart = source.indexOf('direct_replace_active_container() {') const functionEnd = source.indexOf('\n}\n\ncontainer_upstream_connection_count()', functionStart) assert.ok(functionStart >= 0 && functionEnd > functionStart, 'direct replacement function is missing') const direct = source.slice(functionStart, functionEnd) const switchTraffic = direct.indexOf('write_proxy_config "${replacement_ip}:3000"') assert.ok(switchTraffic > 0, 'direct traffic switch is missing') for (const gate of [ 'verify_transcode_lease_schema "$name"', 'verify_task_detail_upload_lease_schema "$name"', 'verify_brand_logo_defaults "$name"', 'verify_material_upload_idempotency_schema "$name"', 'verify_candidate_database_identity "$name"', 'verify_candidate_brand_and_storage_dependencies "$name" "$replacement_ip"', 'verify_video_transcode_capability "$name"', 'verify_task_detail_upload_capability "$name"', 'verify_detail_generator_schema "$name"', 'verify_detail_generator_dependencies "$name"', ]) { const position = direct.indexOf(gate) assert.ok(position > 0 && position < switchTraffic, `${gate} must run before the direct traffic switch`) } assert.match(direct, /fail_direct_replace_with_restore "New direct container database identity differs/) assert.match(direct, /fail_direct_replace_with_restore "New direct container cannot authenticate to S3/) assert.ok(direct.indexOf('prepare_detail_generator_release') < direct.indexOf('docker stop --time 10'), 'configuration and private volume must be validated before stopping the old slot') }) function deploySource() { return readFileSync(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8').replace(/\r\n/g, '\n') } function shellFunction(name) { const source = deploySource() const start = source.indexOf(`${name}() {`) const end = source.indexOf('\n}\n\n', start) assert.ok(start >= 0 && end > start, `missing ${name}`) return source.slice(start, end + 3) } test('detail beta configuration validates enabled key/base without exposing provider secrets', () => { const source = shellFunction('prepare_detail_generator_release') const code = source.match(/<<'NODE'\n([\s\S]*?)\nNODE/)[1] const env = { ...process.env, DETAIL_GENERATOR_ENABLED: 'true', DETAIL_GENERATOR_API_KEY: 'synthetic-private-key', DETAIL_GENERATOR_BASE_URL: 'https://model.example.invalid/v1', DETAIL_GENERATOR_STORAGE_DIR: '/app/detail-generator-private' } for (const input of [ { env, status: 0, output: 'true' }, { env: { ...env, DETAIL_GENERATOR_API_KEY: '' }, status: 1, output: '' }, { env: { ...env, DETAIL_GENERATOR_BASE_URL: '' }, status: 1, output: '' }, { env: { ...env, DETAIL_GENERATOR_BASE_URL: 'https://user:synthetic-private-key@model.example.invalid' }, status: 1, output: '' }, { env: { ...env, DETAIL_GENERATOR_ENABLED: '', DETAIL_GENERATOR_API_KEY: '' }, status: 0, output: 'false' }, { env: { ...env, DETAIL_GENERATOR_ENABLED: 'false', DETAIL_GENERATOR_API_KEY: '' }, status: 0, output: 'false' }, ]) { const result = spawnSync(process.execPath, ['-e', code], { env: input.env, encoding: 'utf8' }) assert.equal(result.status, input.status) assert.equal(result.stdout, input.output) assert.doesNotMatch(result.stdout + result.stderr, /synthetic-private-key/) } assert.match(source, /--env-file "\$ENV_FILE_PATH"/) assert.doesNotMatch(source, /-e [^\n]*DETAIL_GENERATOR_API_KEY/) }) test('fixed private storage rejects overlapping uploads before creating directories', () => { const check = shellFunction('detail_private_mount_is_safe') const privateRoot = '/mnt/PoolOne/apps/youyidian-3d/detail-generator-private' for (const upload of [privateRoot, '/mnt/PoolOne/apps/youyidian-3d', `${privateRoot}/uploads`]) { const result = spawnSync(bash, ['-s'], { input: `DETAIL_PRIVATE_MOUNT='${privateRoot}'\nUPLOADS_MOUNT='${upload}'\n${check}\ndetail_private_mount_is_safe\n`, encoding: 'utf8' }) assert.equal(result.status, 1, `overlap accepted: ${upload}`) } assert.match(check, /while \[ "\$ancestor" != "\/" \]/) assert.match(check, /\[ ! -L "\$ancestor" \]/) const prepare = shellFunction('prepare_detail_generator_release') assert.match(prepare, /install -d -m 0700 -o 1001 -g 1001/) assert.match(prepare, /1001:1001:700/) }) test('enabled containers bind private persistence and verify actual Node22 PSD/PNG without model calls', () => { const create = shellFunction('create_container') assert.match(create, /if \[ "\$DETAIL_FEATURE_ENABLED" = "true" \]/) assert.match(create, /--mount "type=bind,source=\$\{DETAIL_PRIVATE_MOUNT\},target=\/app\/detail-generator-private"/) assert.match(create, /--env-file "\$sanitized_env_file"/) assert.doesNotMatch(create, /DETAIL_GENERATOR_API_KEY/) const schema = shellFunction('verify_detail_generator_schema') assert.match(schema, /20261012_detail_generator/) assert.match(schema, /completed\[0\]\.checksum !== checksum/) for (const table of ['projects', 'assets', 'jobs', 'brand_profiles']) assert.match(schema, new RegExp(`detail_generator_${table}`)) const runtime = shellFunction('verify_detail_generator_dependencies') assert.match(runtime, /\/opt\/detail-renderer\/bin\/node/) assert.match(runtime, /!== 22/) assert.match(runtime, /renderRequest/) assert.match(runtime, /format:'psd'/) assert.match(runtime, /format:'png'/) assert.match(runtime, /8BPS/) assert.match(runtime, /\.deploy-probe-/) assert.doesNotMatch(runtime, /fetch\(|generateDetailImage|runDetailGeneratorWorker/) }) test('AI model cutover gate reads both migration checksums and rejects missing or mistyped capability state', async () => { const code = shellFunction('verify_detail_generator_schema').match(/<<'NODE'\n([\s\S]*?)\nNODE/)[1] const columns = [ ...aiModelRegistryEvidence().columns, ...Object.entries({ detail_generator_projects: ['id', 'owner_id', 'document', 'revision', 'deleted_at'], detail_generator_assets: ['id', 'project_id', 'owner_id', 'storage_key', 'byte_size'], detail_generator_jobs: ['id', 'project_id', 'owner_id', 'status', 'idempotency_key', 'fingerprint', 'project_revision', 'payload', 'checkpoint', 'lease_token', 'lease_expires_at'], detail_generator_brand_profiles: ['id', 'name', 'profile', 'revision'], }).flatMap(([table_name, names]) => names.map(column_name => ({ table_name, column_name }))), ] const checksum = createHash('sha256').update('schema fixture\n').digest('hex') const run = async (changedColumns = columns, missingMigration = '') => { const state = { exitCode: 0 }, checked = [] class PrismaClient { async $queryRawUnsafe(sql, name) { if (sql.includes('_prisma_migrations')) { checked.push(name); return name === missingMigration ? [] : [{ checksum }] } return changedColumns } async $disconnect() {} } await runInNewContext(code, { process: state, require: name => ({ fs: { readFileSync: () => 'schema fixture\r\n' }, crypto: { createHash }, '@prisma/client': { PrismaClient } })[name] }) return { ...state, checked } } const accepted = await run() assert.equal(accepted.exitCode, 0) assert.deepEqual(accepted.checked, ['20261012_detail_generator', '20261012_ai_model_registry', '20261013_ai_model_capabilities']) for (const migration of ['20261012_ai_model_registry', '20261013_ai_model_capabilities']) assert.equal((await run(columns, migration)).exitCode, 1) assert.equal((await run(columns.filter(row => row.column_name !== 'capability_metadata'))).exitCode, 1) assert.equal((await run(columns.map(row => row.column_name === 'capability_metadata' ? { ...row, data_type: 'text' } : row))).exitCode, 1) }) test('AI registry migration files are present in builder and immutable runner verification scope', () => { const dockerfile = readFileSync(new URL('../Dockerfile', import.meta.url), 'utf8') for (const migration of ['20261012_ai_model_registry', '20261013_ai_model_capabilities']) { assert.ok(dockerfile.includes(`test -f "/app/prisma/migrations/${migration}/migration.sql"`)) assert.ok(dockerfile.includes(`COPY --from=builder --chown=root:root /app/prisma/migrations/${migration} ./prisma/migrations/${migration}`)) } }) test('detail uploads keep multipart headroom without expanding all proxy routes', () => { assert.match(deploySource(), /location ~ "\^\/api\/detail-generator\/projects\/\[0-9a-fA-F-\]\{36\}\/assets\\\$" \{\n\s+client_max_body_size 17m;\n\s+proxy_request_buffering off;/) }) test('brand knowledge upload stays bounded in the route and stable proxy', () => { const deploySource = readFileSync(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8') .replace(/\r\n/g, '\n') const routeSource = readFileSync( new URL('../app/api/department-dashboard/brand-knowledge/route.ts', import.meta.url), 'utf8', ) assert.match( deploySource, /location = \/api\/department-dashboard\/brand-knowledge \{\n\s+client_max_body_size 53m;\n\s+proxy_request_buffering off;/, ) const headerGuard = routeSource.indexOf('assertBoundedMultipartHeaders(') const acquireSlot = routeSource.indexOf('boundedMultipartRequestLimiter.acquire()') const parseBody = routeSource.indexOf('request.formData()') const parsedFiles = routeSource.indexOf('assertMultipartFiles(', parseBody) assert.ok(headerGuard > 0) assert.ok(acquireSlot > headerGuard) assert.ok(parseBody > acquireSlot) assert.ok(parsedFiles > parseBody) assert.match(routeSource, /BRAND_KNOWLEDGE_MAX_MAIN_FILE_BYTES/) assert.match(routeSource, /BRAND_KNOWLEDGE_MAX_REQUEST_BYTES/) assert.match(routeSource, /resolveOwnedMergedUpload/) assert.match(routeSource, /publishMergedUploadByHardLink/) assert.doesNotMatch(routeSource, /formData\.get\('file'\)/) assert.match(routeSource, /import \{ isWebFileLike \} from '@\/lib\/web-file'/) assert.doesNotMatch(routeSource, /instanceof File/) assert.match(routeSource, /releaseRequestSlot\?\.\(\)/) })