132 lines
6.2 KiB
JavaScript
132 lines
6.2 KiB
JavaScript
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\?\.\(\)/)
|
|
})
|