furniture-3d-system-Wu/scripts/tests/yydd-prod-release-guards.test.mjs
WuYongJie 928a7b37a9 feat(ai): use durable Relay native image tasks
Persist image submissions with fixed idempotency keys, leased execution, provider
identity, reference snapshots and staged results. Integrate DIY, assisted assembly
and non-integrated white-background sessions without holding generation in a
browser request. Recover ambiguous submissions through owner-scoped reads and
stop a new eight-view batch when restoring an earlier request.

Add the NativeImageGenerationJob migration and a dedicated worker to both release
entry points. Protect job inputs/results from storage GC. Existing production
configuration and business data are not changed by this commit.

Validation: 256 focused Jest tests, isolated PostgreSQL migration/lease/transaction
checks (8), deployment guard tests (9), type-check, production build and desktop/
mobile browser component scenarios. Live native 2K reference-image request worked;
reusing its idempotency key returned the same task. Build uses local placeholders
for otherwise absent Drawing System configuration. Three unrelated material tests
also fail with the original mainline test file; they remain outside this change.

Architecture/visual/root-cause review: passed with warnings. Independent review
completed for the integration and shared input/storage paths; worker additionally
reviewed by the primary agent. Production merge/deployment requires authorization.
2026-10-15 14:55:46 +08:00

193 lines
9.7 KiB
JavaScript

import assert from 'node:assert/strict'
import { execFile, execFileSync } from 'node:child_process'
import { createServer } from 'node:http'
import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import test from 'node:test'
const exec = promisify(execFile)
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const readText = async file => (await readFile(file, 'utf8')).replaceAll('\r\n', '\n')
const guards = await readText(join(root, 'scripts/yydd-prod-release-guards.sh'))
const bash = process.env.DEPLOY_TEST_BASH || (process.platform === 'win32'
? 'C:/Program Files/Git/bin/bash.exe' : 'bash')
const python = process.env.DEPLOY_TEST_PYTHON || 'python3'
const scratch = await mkdtemp(join(process.env.DEPLOY_TEST_TMP || tmpdir(), 'release-guards-'))
const quote = value => `'${value.replaceAll("'", "'\\''")}'`
const posix = value => value.replaceAll('\\', '/')
const run = command => exec(bash, ['-c', `set -euo pipefail\n${guards}\n${command}`])
test('missing server and empty static output are rejected before copying', async () => {
const app = join(scratch, 'incomplete')
await mkdir(join(app, '.next/static'), { recursive: true })
await mkdir(join(app, '.next/standalone'), { recursive: true })
await assert.rejects(run(`prepare_standalone_runtime ${quote(posix(app))}`))
await writeFile(join(app, '.next/standalone/server.js'), 'server')
await assert.rejects(run(`prepare_standalone_runtime ${quote(posix(app))}`))
await writeFile(join(app, '.next/static/empty.js'), '')
await assert.rejects(run(`prepare_standalone_runtime ${quote(posix(app))}`))
})
test('complete output is copied and can be checked without recopying', async () => {
const app = join(scratch, 'complete')
await mkdir(join(app, '.next/static'), { recursive: true })
await mkdir(join(app, '.next/standalone'), { recursive: true })
await mkdir(join(app, 'public'), { recursive: true })
await writeFile(join(app, '.next/standalone/server.js'), 'server')
await writeFile(join(app, '.next/static/app.js'), 'script')
await writeFile(join(app, 'public/logo.png'), 'asset')
await run(`prepare_standalone_runtime ${quote(posix(app))}\nassert_release_ready ${quote(posix(app))}`)
assert.equal(await readFile(join(app, '.next/standalone/.next/static/app.js'), 'utf8'), 'script')
assert.equal(await readFile(join(app, '.next/standalone/public/logo.png'), 'utf8'), 'asset')
})
test('both generated deployment paths prepare before switching and retain failure evidence', async () => {
for (const file of ['deploy-yydd-prod-from-git.ps1', 'create-yydd-prod-deploy.ps1']) {
const source = await readText(join(root, 'scripts', file))
const shell = source.match(/\$shellScript = @'\r?\n([\s\S]*?)\r?\n'@/)[1]
.replace('__RELEASE_GUARDS__', () => guards)
execFileSync(bash, ['-n'], { input: shell })
assert(shell.includes('acquire_deploy_lock\n'))
assert(shell.includes('assert_release_ready "$app_root" || return 1'))
const switchAt = shell.lastIndexOf('ln -sfn "$RELEASE_DIR" "$CURRENT"')
assert(shell.indexOf('prepare_standalone_runtime "$APP_DIR"') < switchAt)
assert(shell.indexOf('assert_release_ready "$APP_DIR"') < switchAt)
assert(shell.indexOf('verify_release_http "http://127.0.0.1:$APP_PORT"') > switchAt)
assert(shell.includes('verify_release_http "https://cp.jiansun.vip"'))
assert(shell.includes('ln -sfn "$PREVIOUS_RELEASE" "$CURRENT"'))
assert(!shell.includes('rm -rf -- "$RELEASE_DIR"'))
assert(!shell.includes('rm -f -- "$HEALTHCHECK_FILE"'))
}
})
test('HTTP verification parses pages and rejects missing or disguised static resources', async () => {
const script = guards.match(/<<'PY'\n([\s\S]*?)\nPY/)[1]
const scriptPath = join(scratch, 'verify.py')
await writeFile(scriptPath, script)
let failure = null
const server = createServer((req, res) => {
if (req.url === '/api/health') {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ success: true, data: { services: { database: 'connected', api: 'running' } } }))
} else if (req.url.startsWith('/_next/static/')) {
res.statusCode = failure === 'missing' ? 404 : 200
res.setHeader('Content-Type', failure === 'html' ? 'text/html' : req.url.endsWith('.css') ? 'text/css' : 'application/javascript')
res.end('test-content')
} else {
res.setHeader('Content-Type', 'text/html')
res.end('<html><link rel="stylesheet" href="/_next/static/main.css"><script src="/_next/static/main.js"></script></html>')
}
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
try {
const url = `http://127.0.0.1:${server.address().port}`
const result = await exec(python, [scriptPath, url])
assert(result.stdout.includes('Health and 2 static assets verified'))
failure = 'missing'
await assert.rejects(exec(python, [scriptPath, url]))
failure = 'html'
await assert.rejects(exec(python, [scriptPath, url]))
} finally {
await new Promise(resolve => server.close(resolve))
}
})
test('a started remote deployment cannot silently retry via archive fallback', async () => {
const source = await readText(join(root, 'scripts/deploy-yydd-prod-from-git.ps1'))
assert(source.includes('$remoteDeployStarted = $true'))
assert(source.includes('if ($remoteDeployStarted) {\n throw\n }'))
assert(source.includes('$Command = $guard + "`n" + $Command'))
})
test('failed post-switch validation restores only the previous release and retains evidence', async () => {
for (const file of ['deploy-yydd-prod-from-git.ps1', 'create-yydd-prod-deploy.ps1']) {
const source = await readText(join(root, 'scripts', file))
const cleanup = source.match(/cleanup_failed_release\(\) \{[\s\S]*?\n\}/)[0]
const base = join(scratch, file.replace('.ps1', ''))
await mkdir(join(base, 'previous'), { recursive: true })
await mkdir(join(base, 'new'), { recursive: true })
await writeFile(join(base, 'check.log'), 'static 404')
await assert.rejects(run(`
BASE=${quote(posix(base))}
CURRENT="$BASE/current"
PREVIOUS_RELEASE="$BASE/previous"
RELEASE_DIR="$BASE/new"
DEPLOY_SUCCEEDED=0
HEALTHCHECK_FILE="$BASE/check.log"
# Model symlink operations so the test also runs without Windows symlink privileges.
ln() { printf '%s\\n' "$2" > "$3"; }
readlink() { cat "$2"; }
ln -sfn "$RELEASE_DIR" "$CURRENT"
start_pm2_release() { readlink -f "$1" > "$BASE/restarted"; }
${cleanup}
trap 'cleanup_failed_release "$?"' EXIT
exit 1
`))
const restarted = await readFile(join(base, 'restarted'), 'utf8')
assert(restarted.trim().endsWith('/previous'))
assert.equal(await readFile(join(base, 'check.log'), 'utf8'), 'static 404')
}
})
test('image worker gets one persistent secret and starts only its own PM2 process', async () => {
const app = join(scratch, 'worker-release')
const shared = join(scratch, 'worker-shared')
await mkdir(join(app, 'scripts'), { recursive: true })
await mkdir(shared, { recursive: true })
await writeFile(join(app, 'scripts/run-ai-image-worker.mjs'), '// worker fixture')
await writeFile(join(shared, '.env'), 'EXISTING_SETTING=keep\n')
await run(`
SHARED=${quote(posix(shared))}
PM2_NAME=furniture-test
APP_PORT=3001
pm2() { printf '%s\\n' "$*" >> "$SHARED/pm2-calls"; }
ensure_ai_image_worker_secret ${quote(posix(app))}
ensure_ai_image_worker_secret ${quote(posix(app))}
start_ai_image_worker ${quote(posix(app))}
`)
const env = await readText(join(shared, '.env'))
assert(env.includes('EXISTING_SETTING=keep\n'))
assert.equal(env.match(/^AI_IMAGE_WORKER_SECRET=/gm)?.length, 1)
assert.match(env, /^AI_IMAGE_WORKER_SECRET=[a-f0-9]{64}$/m)
const calls = await readText(join(shared, 'pm2-calls'))
assert(calls.includes('delete furniture-test-ai-image-worker\n'))
assert(calls.includes('start /bin/bash --name furniture-test-ai-image-worker -- -lc '))
assert(calls.includes('exec env PORT=3001 node scripts/run-ai-image-worker.mjs'))
assert(!calls.includes('delete all'))
await run(`SHARED=${quote(posix(shared))}\nensure_ai_image_worker_secret ${quote(posix(app))}`)
assert.equal(await readText(join(shared, '.env')), env)
})
test('rolling back to a release without the worker removes only the worker and does not rotate credentials', async () => {
const app = join(scratch, 'old-worker-release')
const shared = join(scratch, 'old-worker-shared')
await mkdir(app, { recursive: true })
await mkdir(shared, { recursive: true })
await writeFile(join(shared, '.env'), 'EXISTING_SETTING=keep\nAI_IMAGE_WORKER_SECRET=unchanged-test-secret\n')
const before = await readText(join(shared, '.env'))
await run(`
SHARED=${quote(posix(shared))}
PM2_NAME=furniture-test
APP_PORT=3001
pm2() { printf '%s\\n' "$*" >> "$SHARED/pm2-calls"; }
ensure_ai_image_worker_secret ${quote(posix(app))}
start_ai_image_worker ${quote(posix(app))}
`)
assert.equal(await readText(join(shared, '.env')), before)
assert.equal(await readText(join(shared, 'pm2-calls')), 'delete furniture-test-ai-image-worker\n')
})
test('a failed secret generator rejects startup preparation without saving an empty credential', async () => {
const app = join(scratch, 'failed-secret-release')
const shared = join(scratch, 'failed-secret-shared')
await mkdir(join(app, 'scripts'), { recursive: true })
await mkdir(shared, { recursive: true })
await writeFile(join(app, 'scripts/run-ai-image-worker.mjs'), '// worker fixture')
await writeFile(join(shared, '.env'), 'EXISTING_SETTING=keep\n')
await assert.rejects(run(`SHARED=${quote(posix(shared))}\nnode() { return 1; }\nensure_ai_image_worker_secret ${quote(posix(app))}`))
assert.equal(await readText(join(shared, '.env')), 'EXISTING_SETTING=keep\n')
})