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/) assert.match(gate, /20261013_ai_model_verifications/) assert.match(gate, /20261020090000_desktop_client_workspace/) }) 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('storage deployment gate rejects broken public bucket URLs before authenticating', async () => { const fn = shellFunction('verify_candidate_authenticated_s3') const code = fn.slice(fn.indexOf("node -e '\n") + "node -e '\n".length, fn.lastIndexOf("\n'")) const base = { S3_ENDPOINT: 'http://minio:9000', S3_BUCKET: 'gongxiangpan', S3_ACCESS_KEY_ID: 'fixture', S3_SECRET_ACCESS_KEY: 'fixture' } for (const domain of ['', 'https://s3.jiansun.vip', 'https://s3.jiansun.vip/other-bucket', 'http://s3.jiansun.vip/gongxiangpan', 'https://user:pass@s3.jiansun.vip/gongxiangpan', 'https://s3.jiansun.vip/gongxiangpan?token=fixture', 'https://s3.jiansun.vip/gongxiangpan#fragment']) { let calls = 0 assert.throws(() => runInNewContext(code, { URL, AbortSignal, process: { env: { ...base, S3_PUBLIC_DOMAIN: domain }, exit() { throw new Error('missing public bucket URL') } }, require() { return { S3Client: class { constructor() { calls++ } }, HeadBucketCommand: class {} } }, }), /S3_PUBLIC_DOMAIN|missing public bucket URL/) assert.equal(calls, 0, 'invalid public URL must fail before touching storage') } }) test('storage deployment gate accepts and authenticates the complete public bucket URL', async () => { const fn = shellFunction('verify_candidate_authenticated_s3') const code = fn.slice(fn.indexOf("node -e '\n") + "node -e '\n".length, fn.lastIndexOf("\n'")) for (const domain of ['https://s3.jiansun.vip/gongxiangpan', 'https://s3.jiansun.vip/gongxiangpan/']) { let calls = 0, destroyed = false await runInNewContext(code, { URL, AbortSignal, process: { env: { S3_ENDPOINT: 'http://minio:9000', S3_BUCKET: 'gongxiangpan', S3_PUBLIC_DOMAIN: domain, S3_ACCESS_KEY_ID: 'fixture', S3_SECRET_ACCESS_KEY: 'fixture' }, stderr: { write(message) { throw new Error(message) } }, exit() { throw new Error('unexpected exit') } }, require() { return { S3Client: class { async send(command) { assert.equal(command.input.Bucket, 'gongxiangpan'); calls++ } destroy() { destroyed = true } }, HeadBucketCommand: class { constructor(input) { this.input = input } } } }, }) assert.equal(calls, 1) assert.equal(destroyed, true) } }) test('direct rollback restores an absent or partial replacement from the immutable image', () => { const rollback = shellFunction('restore_direct_previous_container') const image = `sha256:${'c'.repeat(64)}` for (const scenario of [ { present: 0, removeFails: 0, status: 0, removed: false }, { present: 1, removeFails: 0, status: 0, removed: true }, { present: 1, removeFails: 1, status: 1, removed: true }, ]) { const input = `set -u present=${scenario.present} remove_fails=${scenario.removeFails} removed=0 docker() { case "$1" in container) [ "$present" = 1 ] ;; rm) removed=1; [ "$remove_fails" = 0 ] ;; start) return 0 ;; *) return 90 ;; esac } create_container() { printf 'created:%s:%s\\n' "$1" "$2"; } verify_container_uploads_mount_metadata() { return 0; } verify_container_upload_dir_config() { return 0; } verify_container_uploads_storage() { return 0; } wait_for_container_health() { printf '172.18.0.9\\n'; } write_proxy_config() { printf 'proxy:%s\\n' "$1"; } switch_proxy() { return 0; } verify_proxy_target() { printf 'proxy-verified\\n'; } write_current_container() { return 0; } resume_video_transcode() { printf 'video-resumed\\n'; } resume_task_detail_upload() { printf 'upload-resumed\\n'; } ${rollback} restore_direct_previous_container material-blue '${image}' status=$? printf 'removed:%s\\n' "$removed" >&2 exit "$status" ` const result = spawnSync(bash, ['-s'], { input, encoding: 'utf8' }) assert.equal(result.status, scenario.status, result.stderr || result.stdout) assert.equal(result.stderr.includes('removed:1'), scenario.removed) if (scenario.status === 0) { assert.match(result.stdout, new RegExp(`created:material-blue:${image}`)) assert.match(result.stdout, /proxy:172\.18\.0\.9:3000/) assert.ok(result.stdout.indexOf('proxy-verified') < result.stdout.indexOf('video-resumed')) assert.ok(result.stdout.indexOf('proxy-verified') < result.stdout.indexOf('upload-resumed')) } else assert.doesNotMatch(result.stdout, /created:|proxy:/) } const invalid = spawnSync(bash, ['-s'], { input: `docker() { echo docker-was-called; }\n${rollback}\nrestore_direct_previous_container material-blue mutable:latest\n`, encoding: 'utf8' }) assert.equal(invalid.status, 1) assert.doesNotMatch(invalid.stdout, /docker-was-called/) const direct = shellFunction('direct_replace_active_container') assert.match(direct, /previous_image="\$\(docker inspect -f '\{\{\.Image\}\}'/) assert.doesNotMatch(direct, /\.Config\.Image/) }) 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 checks every migration and rejects missing, mistyped or wrongly nullable verification 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', '20261013_ai_model_verifications', '20261020090000_desktop_client_workspace']) for (const migration of ['20261012_ai_model_registry', '20261013_ai_model_capabilities', '20261013_ai_model_verifications']) 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) assert.equal((await run(columns.filter(row => row.table_name !== 'ai_model_verifications'))).exitCode, 1, 'missing verification table must block cutover') for (const field of ['stage', 'context_hash', 'task_id', 'evidence']) { const target = row => row.table_name === 'ai_model_verifications' && row.column_name === field assert.equal((await run(columns.filter(row => !target(row)))).exitCode, 1, `missing ${field} must block cutover`) assert.equal((await run(columns.map(row => target(row) ? { ...row, data_type: 'boolean' } : row))).exitCode, 1, `mistyped ${field} must block cutover`) assert.equal((await run(columns.map(row => target(row) ? { ...row, is_nullable: row.is_nullable === 'YES' ? 'NO' : 'YES' } : row))).exitCode, 1, `wrong nullability for ${field} must block cutover`) } }) 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', '20261013_ai_model_verifications']) { 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('desktop private image sync supports the route 20MiB limit without widening other endpoints', () => { const source=deploySource() const marker='location ~ "^/api/desktop/projects/desktop/[A-Za-z0-9_-]{1,80}/assets/[^/]+\\$" {' const start=source.indexOf(marker) assert.ok(start>=0) const location=source.slice(start,source.indexOf('\n }',start)) assert.match(location,/client_max_body_size 21m;/) assert.match(location,/proxy_request_buffering off;/) assert.match(location,/proxy_set_header Host \\\$http_host;/) }) 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\?\.\(\)/) })