import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { spawnSync } from 'node:child_process' import test from 'node:test' 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, /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/) }) test('direct replacement runs database and dependency gates before switching traffic', () => { const source = readFileSync(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8') 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"', ]) { 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/) }) 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\?\.\(\)/) })