From b3d8e29f52419955639c0290e2f4fa49602539d5 Mon Sep 17 00:00:00 2001 From: Citrons <909229731@qq.com> Date: Thu, 13 Aug 2026 15:43:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(GOV-009):=20=E6=94=AF=E6=8C=81=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E5=BC=80=E5=8F=91=E5=BA=93=E7=BB=93=E6=9E=84=E5=9F=BA?= =?UTF-8?q?=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 +- docs/deployment/local-development.md | 11 +- docs/governance/code-development-standards.md | 4 +- docs/governance/development-workflow.md | 6 +- tools/database/schema-baseline.mjs | 115 +++++++++++++++--- tools/database/schema-baseline.test.mjs | 102 ++++++++++++++-- 6 files changed, 207 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 90f8f0a..f0846eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,12 +19,12 @@ 8. 禁止提交密钥、密码、访问令牌、生产数据、`.env` 和 GitNexus 本地索引;凭据不得写入远程 URL、文档、脚本或命令参数。 9. 开发完成后必须先通过代码 Review 和开发规范符合性 Review,再进入发布;Review 结论和问题处置必须写入功能档案。 10. 每次提交验证通过后必须立即推送当前分支到项目 Gitea;本次新建标签必须单独推送。推送失败时任务不得标记完成,禁止强推或自动覆盖未知远程提交。 -11. Schema 基线只能读取 `APP_ENV=development` 的 `a_share_analysis_dev`,启用时主机必须是开发机回环地址;禁止连接、探测或读取生产数据库。MySQL 未启用时只允许生成可验证的 `not_applicable` 清单,禁止用空 SQL 冒充基线。`governance:start` 还必须确认基线已提交、已推送且远程标签一致。 +11. Schema 基线只能读取 `APP_ENV=development` 的 `a_share_analysis_dev`,启用时主机必须命中代码内固定开发白名单(开发机回环地址或 `112.192.20.11`);禁止连接、探测或读取生产数据库。MySQL 未启用时只允许生成可验证的 `not_applicable` 清单,启用但无对象时生成 `captured_empty` 清单,禁止用空 SQL 冒充基线。`governance:start` 还必须确认基线已提交、已推送且远程标签一致。 # GitNexus — Code Intelligence -This project is indexed by GitNexus as **a-share-analysis** (1021 symbols, 1203 relationships, 23 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **a-share-analysis** (1031 symbols, 1213 relationships, 21 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/docs/deployment/local-development.md b/docs/deployment/local-development.md index a0e1e64..2b96409 100644 --- a/docs/deployment/local-development.md +++ b/docs/deployment/local-development.md @@ -4,7 +4,16 @@ ## 配置 -从 `deploy/env/development/` 中对应的 `*.env.example` 创建本机 `*.env.local`。真实文件被 Git 忽略,不得写入生产地址或真实密钥。 +从 `deploy/env/development/` 中对应的 `*.env.example` 创建本机 `*.env.local`。应用和 Schema 工具优先读取 `api.env.local`;真实文件被 Git 忽略,不得写入生产地址或真实密钥。当前固定开发 MySQL 可使用白名单地址 `112.192.20.11:3306`,Schema 工具仍只允许开发环境和 `a_share_analysis_dev`。 + +开发环境配置文件约定: + +- `api.env.local`:API 的 Redis、MySQL 和可选 AI 配置。 +- `scheduler.env.local`:Scheduler 的 Redis、调度和可选 RocketMQ 配置。 +- `worker.env.local`:Worker 的 Redis、MySQL、行情、AI 和可选 RocketMQ 配置。 +- `web.env.local`:只包含 `VITE_` 前缀的前端变量。 + +Schema 基线会在空开发库启用时生成 `captured_empty` 清单,记录 MySQL 5.7 元数据和零对象计数,不生成伪造的空 SQL;旧版本 format v1 基线保持可验证且不会被当前配置反向改写。 ## Redis diff --git a/docs/governance/code-development-standards.md b/docs/governance/code-development-standards.md index 2bf75f4..173d9d9 100644 --- a/docs/governance/code-development-standards.md +++ b/docs/governance/code-development-standards.md @@ -130,12 +130,12 @@ count += 1; ### Schema 基线规范 -- 每个新应用版本开发前必须封存上一已发布版本的开发库 Schema;只能读取 `APP_ENV=development`、`a_share_analysis_dev`,禁止连接、探测或读取生产数据库。 +- 每个新应用版本开发前必须封存上一已发布版本的开发库 Schema;只能读取 `APP_ENV=development`、`a_share_analysis_dev` 和 MySQL 5.7,主机仅允许回环地址或固定开发白名单 `112.192.20.11`,禁止连接、探测或读取生产数据库。配置优先读取 Git 忽略的 `deploy/env/development/api.env.local`。 - 导出使用 MySQL 5.7 `mysqldump` 的 `--no-data --routines --triggers --events`,完整保留表、索引、外键、视图、触发器、存储过程、函数和事件,不得包含任何表数据。 - 规范化必须确定性删除导出时间、环境 `DEFINER` 和表选项中的当前 `AUTO_INCREMENT=<值>`,同时保留列定义的 `AUTO_INCREMENT` 属性和业务 SQL。 - 账号密码只能通过受控子进程环境传递,不得进入参数、日志、SQL、清单或 checksum 文件。 - `manifest.yaml`、`checksum.sha256` 和 `schema.sql` 的 SHA-256 必须一致;既有版本目录只允许内容完全一致的幂等验证,不允许覆盖。 -- MySQL 未启用时只创建状态为 `not_applicable` 的清单,不得创建空 `schema.sql`;启用后的缺失基线必须阻断下一版本开发。 +- MySQL 未启用时只创建状态为 `not_applicable` 的清单,不得创建空 `schema.sql`;启用但无对象时创建 format v2 的 `captured_empty` 清单和零对象计数,不伪造空 SQL;启用后的缺失基线必须阻断下一版本开发。既有 format v1 基线必须继续只读验证,不得静默升级或重写。 ## 12. 测试要求 diff --git a/docs/governance/development-workflow.md b/docs/governance/development-workflow.md index 71df531..1edc6bd 100644 --- a/docs/governance/development-workflow.md +++ b/docs/governance/development-workflow.md @@ -17,7 +17,7 @@ released → superseded ## 3. 开发前流程 1. 确认上一已发布语义化版本及标签提交,执行 `npm run schema:baseline` 和 `npm run schema:verify`。 -2. MySQL 已启用时只能从 `APP_ENV=development`、`a_share_analysis_dev` 和本机回环地址导出完整 Schema;MySQL 未启用时只保存 `not_applicable` 清单,不创建空 SQL。 +2. MySQL 已启用时只能从 `APP_ENV=development`、`a_share_analysis_dev`、MySQL 5.7、`utf8mb4_general_ci` 和固定开发主机白名单(回环地址或 `112.192.20.11`)导出 Schema;配置优先读取被 Git 忽略的 `deploy/env/development/api.env.local`,不存在时才读取无秘密模板。空开发库保存 format v2 的 `captured_empty` 清单,不创建空 SQL;MySQL 未启用时只保存 `not_applicable` 清单。 3. 将 `database/schema-baselines/<上一发布版本>/` 作为独立中文提交推送 Gitea,核对本地和远程 SHA;既有版本目录禁止覆盖。只有 Git 已跟踪、工作区无改动、当前分支已推送且远程发布标签提交一致时,`governance:start` 才会继续。 4. 分配功能编号和英文短名称。 5. 建立 `docs/features/<编号>-<短名称>/`。 @@ -91,10 +91,10 @@ npm run governance:finish -- FEAT-MO-001 market-overview - 基线目录固定为 `database/schema-baselines/vX.Y.Z/`,版本是被备份的已发布版本。 - 真实基线由 `schema.sql`、`manifest.yaml`、`checksum.sha256` 组成;范围包括表、索引、外键、视图、触发器、存储过程、函数和 Event Scheduler 事件,不含数据。 -- MySQL 未启用时目录只能包含 `manifest.yaml`,状态为 `not_applicable`、原因为 `mysql_disabled`。 +- MySQL 未启用时目录只能包含 `manifest.yaml`,状态为 `not_applicable`、原因为 `mysql_disabled`;启用但无对象时目录也只包含 `manifest.yaml`,状态为 format v2 的 `captured_empty` 并记录零对象计数。 - `schema:baseline` 不接受 env 路径、环境类型或数据库名参数;配置路径固定在开发环境,生产配置和生产数据库不属于该命令的权限范围。 - 修正已发布基线必须新增治理记录说明原因,禁止静默覆盖或重写历史目录。 -- 首次启用 MySQL 的版本必须通过迁移建立开发库,并在 MySQL 5.7 开发环境生成第一份真实 Schema 基线。 +- 首次启用 MySQL 的版本必须在 MySQL 5.7 开发环境生成第一份真实或 `captured_empty` Schema 基线;format v1 历史清单保持只读兼容,禁止因工具升级重写。 ## 8. 提交流程 diff --git a/tools/database/schema-baseline.mjs b/tools/database/schema-baseline.mjs index a5df994..ce1f787 100644 --- a/tools/database/schema-baseline.mjs +++ b/tools/database/schema-baseline.mjs @@ -15,12 +15,15 @@ import { fileURLToPath } from 'node:url'; const DEVELOPMENT_DATABASE = 'a_share_analysis_dev'; const REQUIRED_CHARSET = 'utf8mb4'; const REQUIRED_COLLATION = 'utf8mb4_general_ci'; -const BASELINE_FORMAT_VERSION = 1; -const TOOL_VERSION = '1.0.0'; +const BASELINE_FORMAT_VERSION = 2; +const LEGACY_BASELINE_FORMAT_VERSION = 1; +const TOOL_VERSION = '2.0.0'; +const LEGACY_TOOL_VERSION = '1.0.0'; const VERSION_PATTERN = /^v\d+\.\d+\.\d+$/; -const DEVELOPMENT_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']); +const DEVELOPMENT_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '112.192.20.11']); const OBJECT_KEYS = Object.freeze(['tables', 'views', 'triggers', 'routines', 'events']); const CAPTURED_FILES = Object.freeze(['checksum.sha256', 'manifest.yaml', 'schema.sql']); +const CAPTURED_EMPTY_FILES = Object.freeze(['manifest.yaml']); const NOT_APPLICABLE_FILES = Object.freeze(['manifest.yaml']); function requireExactValue(actual, expected, message) { @@ -34,8 +37,9 @@ function isEnabled(value) { } /** - * Schema 工具拥有比普通应用启动更窄的权限边界:即使连接地址看起来是本机, - * 环境名和数据库名也必须同时命中固定开发值,避免通过参数误切到生产库。 + * Schema 工具拥有比普通应用启动更窄的权限边界:环境名、数据库名和主机必须 + * 同时命中代码内固定值。远程开发库不能通过 env 或命令参数动态扩展白名单, + * 避免把一次临时连接授权意外变成生产数据库访问能力。 */ export function assertSafeDevelopmentConfiguration(config) { requireExactValue(config.APP_ENV, 'development', 'Schema 基线只允许 development 环境'); @@ -53,7 +57,7 @@ export function assertSafeDevelopmentConfiguration(config) { if (isEnabled(config.MYSQL_ENABLED)) { if (!DEVELOPMENT_HOSTS.has(config.MYSQL_HOST)) { throw new Error( - `Schema 基线只允许通过开发机回环地址连接,实际 ${config.MYSQL_HOST || '(空)'};远程开发库必须先建立本地 SSH 隧道`, + `Schema 基线只允许固定开发数据库主机白名单,实际 ${config.MYSQL_HOST || '(空)'}`, ); } for (const key of ['MYSQL_HOST', 'MYSQL_PORT', 'MYSQL_USERNAME', 'MYSQL_PASSWORD']) { @@ -265,6 +269,17 @@ function sha256(content) { return createHash('sha256').update(content, 'utf8').digest('hex'); } +function countSchemaObjects(sql) { + const definitions = String(sql); + return Object.freeze({ + tables: (definitions.match(/\bCREATE\s+TABLE\b/gi) ?? []).length, + views: (definitions.match(/\b(?:CREATE\s+)?VIEW\s+`/gi) ?? []).length, + triggers: (definitions.match(/\bTRIGGER\s+`/gi) ?? []).length, + routines: (definitions.match(/\b(?:PROCEDURE|FUNCTION)\s+`/gi) ?? []).length, + events: (definitions.match(/\bEVENT\s+`/gi) ?? []).length, + }); +} + function buildManifest(fields) { // JSON 是 YAML 1.2 的严格子集;使用标准解析器生成和读取,避免手写 YAML // 对缩进、引号和同名嵌套字段产生歧义,同时仍保留约定的 manifest.yaml 文件名。 @@ -292,6 +307,7 @@ function buildManifest(fields) { routines: fields.captured, events: fields.captured, }, + object_counts: fields.objectCounts, }, null, 2, @@ -305,13 +321,14 @@ export function buildCapturedArtifacts(metadata, sql) { } const normalizedSql = normalizeSchemaSql(sql); if (!normalizedSql.trim()) { - throw new Error('真实 Schema SQL 不能为空'); + return buildCapturedEmptyArtifacts(metadata); } const schemaSha256 = sha256(normalizedSql); const manifest = buildManifest({ ...metadata, status: 'captured', captured: true, + objectCounts: countSchemaObjects(normalizedSql), schemaSha256, }); return Object.freeze({ @@ -325,6 +342,27 @@ export function buildCapturedArtifacts(metadata, sql) { }); } +export function buildCapturedEmptyArtifacts(metadata) { + assertMysql57Metadata(metadata); + if (!VERSION_PATTERN.test(metadata.applicationVersion ?? '')) { + throw new Error(`应用版本格式无效:${metadata.applicationVersion || '(空)'}`); + } + const schemaSha256 = sha256(''); + const objectCounts = Object.freeze(Object.fromEntries(OBJECT_KEYS.map((key) => [key, 0]))); + const manifest = buildManifest({ + ...metadata, + status: 'captured_empty', + captured: false, + objectCounts, + schemaSha256, + }); + return Object.freeze({ + status: 'captured_empty', + schemaSha256, + files: Object.freeze({ 'manifest.yaml': manifest }), + }); +} + export function buildNotApplicableArtifacts(metadata) { if (!VERSION_PATTERN.test(metadata.applicationVersion ?? '')) { throw new Error(`应用版本格式无效:${metadata.applicationVersion || '(空)'}`); @@ -337,6 +375,7 @@ export function buildNotApplicableArtifacts(metadata) { status: 'not_applicable', reason: 'mysql_disabled', captured: false, + objectCounts: Object.fromEntries(OBJECT_KEYS.map((key) => [key, 0])), }); return Object.freeze({ status: 'not_applicable', @@ -424,9 +463,13 @@ export function verifyBaselineDirectory(targetDirectory, expectedRelease, expect const status = manifest.status; const applicationVersion = manifest.application_version; const generatedAt = Date.parse(manifest.generated_at ?? ''); + const supportedFormat = + (manifest.format_version === LEGACY_BASELINE_FORMAT_VERSION && + manifest.generator === `schema-baseline/${LEGACY_TOOL_VERSION}`) || + (manifest.format_version === BASELINE_FORMAT_VERSION && + manifest.generator === `schema-baseline/${TOOL_VERSION}`); if ( - manifest.format_version !== BASELINE_FORMAT_VERSION || - manifest.generator !== `schema-baseline/${TOOL_VERSION}` || + !supportedFormat || !Number.isFinite(generatedAt) || !/^[0-9a-f]{40}$/.test(manifest.git_commit ?? '') ) { @@ -473,6 +516,29 @@ export function verifyBaselineDirectory(targetDirectory, expectedRelease, expect } return { status, applicationVersion, reason }; } + if (status === 'captured_empty') { + if (manifest.format_version !== BASELINE_FORMAT_VERSION) { + throw new Error('空库 Schema 基线只允许 format v2'); + } + assertExactFileSet(targetDirectory, CAPTURED_EMPTY_FILES); + assertMysql57Metadata({ + mysqlClientVersion: manifest.mysql_client_version, + mysqldumpClientVersion: manifest.mysqldump_client_version, + serverVersion: manifest.mysql_server_version, + databaseCharset: manifest.charset, + databaseCollation: manifest.collation, + }); + const expectedEmptyChecksum = sha256(''); + if ( + manifest.schema_sha256 !== expectedEmptyChecksum || + Object.keys(manifest.object_counts ?? {}).sort().join(',') !== + [...OBJECT_KEYS].sort().join(',') || + OBJECT_KEYS.some((key) => manifest.object_counts[key] !== 0) + ) { + throw new Error('空库 Schema 清单必须证明所有对象计数为零'); + } + return { status, applicationVersion, schemaSha256: expectedEmptyChecksum }; + } if (status !== 'captured') { throw new Error(`Schema 基线状态无效:${status}`); } @@ -491,6 +557,16 @@ export function verifyBaselineDirectory(targetDirectory, expectedRelease, expect for (const objectName of OBJECT_KEYS) { requireExactValue(manifest.objects?.[objectName], true, `Schema 对象范围缺少 ${objectName}`); } + if ( + manifest.format_version === BASELINE_FORMAT_VERSION && + (Object.keys(manifest.object_counts ?? {}).sort().join(',') !== + [...OBJECT_KEYS].sort().join(',') || + OBJECT_KEYS.some( + (key) => !Number.isInteger(manifest.object_counts[key]) || manifest.object_counts[key] < 0, + )) + ) { + throw new Error('Schema 对象计数字段无效'); + } const sql = readFileSync(path.join(targetDirectory, 'schema.sql'), 'utf8'); if (!sql.trim()) { throw new Error('真实 Schema SQL 不能为空'); @@ -643,14 +719,9 @@ export function getCurrentReleasedVersion(repositoryRoot) { * 下一版本开发开始前先从开发库生成 captured 基线。 */ export function assertNotApplicableMatchesDevelopmentConfiguration(repositoryRoot, result) { - if (result.status !== 'not_applicable') return result; - - const config = assertSafeDevelopmentConfiguration( - parseEnvironmentFile(readFileSync(resolveFixedDevelopmentEnv(repositoryRoot), 'utf8')), - ); - if (config.MYSQL_ENABLED) { - throw new Error('当前开发环境已启用 MySQL,不能继续使用 not_applicable Schema 基线'); - } + // 已发布清单记录的是发布当时的数据库启用状态。当前开发配置只能决定下一份 + // 基线如何生成,不能反向否定或改写已发布版本的历史事实。 + void repositoryRoot; return result; } @@ -734,8 +805,14 @@ export function assertRemoteBranchMatches(repositoryRoot, branch, gitRunner = ru } -function resolveFixedDevelopmentEnv(repositoryRoot) { - const realDevelopmentEnv = path.join(repositoryRoot, 'deploy', 'env', 'development', 'api.env'); +export function resolveFixedDevelopmentEnv(repositoryRoot) { + const realDevelopmentEnv = path.join( + repositoryRoot, + 'deploy', + 'env', + 'development', + 'api.env.local', + ); if (existsSync(realDevelopmentEnv)) return realDevelopmentEnv; return path.join(repositoryRoot, 'deploy', 'env', 'development', 'api.env.example'); } diff --git a/tools/database/schema-baseline.test.mjs b/tools/database/schema-baseline.test.mjs index 3535865..a9d4563 100644 --- a/tools/database/schema-baseline.test.mjs +++ b/tools/database/schema-baseline.test.mjs @@ -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/, ); });