feat(GOV-009): 支持固定开发库结构基线

This commit is contained in:
2026-08-13 15:43:06 +08:00
parent c5a6e6a2c0
commit b3d8e29f52
6 changed files with 207 additions and 35 deletions
+94 -8
View File
@@ -11,11 +11,13 @@ import {
assertCurrentReleasedBaselinePublished,
assertSafeDevelopmentConfiguration,
buildCapturedArtifacts,
buildCapturedEmptyArtifacts,
buildMysqldumpArguments,
buildNotApplicableArtifacts,
normalizeSchemaSql,
persistBaselineArtifacts,
runBaselineGeneration,
resolveFixedDevelopmentEnv,
assertRemoteBranchMatches,
verifyBaselineDirectory,
} from './schema-baseline.mjs';
@@ -76,10 +78,37 @@ test('拒绝任何非开发环境和非固定开发库配置', () => {
...validConfig,
MYSQL_HOST: 'forbidden-remote.example.invalid',
}),
/开发机回环地址/,
/固定开发数据库主机白名单/,
);
});
test('固定远程开发库白名单只接受指定地址', () => {
assert.doesNotThrow(() =>
assertSafeDevelopmentConfiguration({
...validConfig,
MYSQL_HOST: '112.192.20.11',
}),
);
for (const host of ['112.192.20.12', 'dev-db.example.invalid', '']) {
assert.throws(
() => assertSafeDevelopmentConfiguration({ ...validConfig, MYSQL_HOST: host }),
/固定开发数据库主机白名单/,
);
}
});
test('本地 Schema 配置优先于无秘密模板', () => {
const root = makeTemporaryDirectory();
const envDirectory = path.join(root, 'deploy', 'env', 'development');
mkdirSync(envDirectory, { recursive: true });
const localEnv = path.join(envDirectory, 'api.env.local');
const templateEnv = path.join(envDirectory, 'api.env.example');
writeFileSync(localEnv, 'APP_ENV=development\n');
writeFileSync(templateEnv, 'APP_ENV=template\n');
assert.equal(resolveFixedDevelopmentEnv(root), localEnv);
});
test('拒绝错误字符集、排序规则和不完整的启用配置', () => {
assert.throws(
() => assertSafeDevelopmentConfiguration({ ...validConfig, MYSQL_CHARSET: 'utf8' }),
@@ -357,7 +386,7 @@ test('幂等重跑仍核对清单提交与发布标签提交', () => {
);
});
test('校验能发现 SQL 被修改且生成阶段拒绝空基线', () => {
test('校验能发现 SQL 被修改且空结构转为 captured_empty', () => {
const root = makeTemporaryDirectory();
const target = path.join(root, 'v0.0.5');
persistBaselineArtifacts(
@@ -367,9 +396,67 @@ test('校验能发现 SQL 被修改且生成阶段拒绝空基线', () => {
writeFileSync(path.join(target, 'schema.sql'), 'CREATE TABLE `tampered` (`id` int);\n');
assert.throws(() => verifyBaselineDirectory(target, 'v0.0.5'), /校验值不一致/);
assert.throws(
() => buildCapturedArtifacts({ ...validMetadata, applicationVersion: 'v0.0.4' }, ''),
/不能为空/,
assert.equal(
buildCapturedArtifacts({ ...validMetadata, applicationVersion: 'v0.0.4' }, '').status,
'captured_empty',
);
});
test('启用的空开发库生成 captured_empty format v2 清单', () => {
const artifacts = buildCapturedEmptyArtifacts({
...validMetadata,
applicationVersion: 'v0.0.8',
});
const manifest = JSON.parse(artifacts.files['manifest.yaml']);
assert.equal(artifacts.status, 'captured_empty');
assert.deepEqual(Object.keys(artifacts.files).sort(), ['manifest.yaml']);
assert.equal(manifest.format_version, 2);
assert.equal(manifest.status, 'captured_empty');
assert.equal(manifest.schema_sha256, createHash('sha256').update('').digest('hex'));
assert.deepEqual(manifest.object_counts, {
tables: 0,
views: 0,
triggers: 0,
routines: 0,
events: 0,
});
});
test('启用入口把 mysqldump 空结构持久化为 captured_empty', () => {
const root = makeTemporaryDirectory();
const envFile = path.join(root, 'development.env');
writeFileSync(
envFile,
Object.entries(validConfig)
.map(([key, value]) => `${key}=${value}`)
.join('\n'),
);
const result = runBaselineGeneration({
repositoryRoot: root,
envFile,
applicationVersion: 'v0.0.8',
gitCommit: validMetadata.gitCommit,
generatedAt: validMetadata.generatedAt,
executeFile(command, args) {
if (args[0] === '--version') {
return `${command} Ver 14.14 Distrib 5.7.44, for Linux`;
}
if (command === 'mysql') {
return '5.7.44-log\tutf8mb4\tutf8mb4_general_ci\n';
}
return '';
},
});
assert.equal(result.status, 'captured_empty');
assert.deepEqual(
readFileSync(
path.join(root, 'database', 'schema-baselines', 'v0.0.8', 'manifest.yaml'),
'utf8',
).includes('captured_empty'),
true,
);
});
@@ -431,7 +518,7 @@ test('不适用清单拒绝未知对象键和伪造的格式元数据', () => {
assert.throws(() => verifyBaselineDirectory(target, 'v0.0.5'), /清单格式/);
});
test('不适用基线只在当前开发环境仍关闭 MySQL 时有效', () => {
test('历史不适用基线不被当前开发配置反向否定', () => {
const root = makeTemporaryDirectory();
const envDirectory = path.join(root, 'deploy', 'env', 'development');
const envFile = path.join(envDirectory, 'api.env.example');
@@ -452,9 +539,8 @@ test('不适用基线只在当前开发环境仍关闭 MySQL 时有效', () => {
.map(([key, value]) => `${key}=${value}`)
.join('\n'),
);
assert.throws(
assert.doesNotThrow(
() => assertNotApplicableMatchesDevelopmentConfiguration(root, result),
/已启用 MySQL.*not_applicable/,
);
});