Commit 4a0c400253c42fd0d25f1038a9dc7fd0e76f8ad5
1 parent
af4ac730
skeleton-gen: 测试库 lane 化——ERP_TEST_DB_SCHEMA env 覆盖 + marker 兜底链 + application*.…
…yml 占位约定 + Flyway out-of-order;env/marker 双缺省行为一字不变
Showing
7 changed files
with
197 additions
and
6 deletions
lib/seed-demo-data-template.test.mjs
| @@ -13,6 +13,8 @@ const TEMPLATE = fileURLToPath(new URL('../skills/plan/skeleton-gen/templates/sc | @@ -13,6 +13,8 @@ const TEMPLATE = fileURLToPath(new URL('../skills/plan/skeleton-gen/templates/sc | ||
| 13 | 13 | ||
| 14 | // 搭一个临时目标项目:scripts/seed-demo-data.mjs + config-vars.yaml + 可选 sql/seed/*.sql,然后跑脚本。 | 14 | // 搭一个临时目标项目:scripts/seed-demo-data.mjs + config-vars.yaml + 可选 sql/seed/*.sql,然后跑脚本。 |
| 15 | // seedFiles 为 null 表示不创建 sql/seed 目录;为对象 { name: content } 表示创建目录并写入这些文件({} = 空目录)。 | 15 | // seedFiles 为 null 表示不创建 sql/seed 目录;为对象 { name: content } 表示创建目录并写入这些文件({} = 空目录)。 |
| 16 | +// env:传给子进程的增量环境变量;laneMarker:非 null 时写 .tmp/erp-lane-db 兜底标记。 | ||
| 17 | +// 基础环境强制剔除 ERP_TEST_DB_SCHEMA——外层 shell 若恰好设了该变量,"缺省行为"用例会被污染。 | ||
| 16 | function run({ | 18 | function run({ |
| 17 | host = '127.0.0.1', | 19 | host = '127.0.0.1', |
| 18 | port = '1', | 20 | port = '1', |
| @@ -20,6 +22,8 @@ function run({ | @@ -20,6 +22,8 @@ function run({ | ||
| 20 | password = 'x', | 22 | password = 'x', |
| 21 | schemaLine = 'schema: erp_dev', | 23 | schemaLine = 'schema: erp_dev', |
| 22 | seedFiles = null, | 24 | seedFiles = null, |
| 25 | + env = {}, | ||
| 26 | + laneMarker = null, | ||
| 23 | } = {}) { | 27 | } = {}) { |
| 24 | const dir = mkdtempSync(join(tmpdir(), 'erp-seed-')) | 28 | const dir = mkdtempSync(join(tmpdir(), 'erp-seed-')) |
| 25 | mkdirSync(join(dir, 'scripts')) | 29 | mkdirSync(join(dir, 'scripts')) |
| @@ -35,7 +39,13 @@ function run({ | @@ -35,7 +39,13 @@ function run({ | ||
| 35 | writeFileSync(join(seedDir, name), content) | 39 | writeFileSync(join(seedDir, name), content) |
| 36 | } | 40 | } |
| 37 | } | 41 | } |
| 38 | - return spawnSync('node', [join(dir, 'scripts', 'seed-demo-data.mjs')], { encoding: 'utf8' }) | 42 | + if (laneMarker !== null) { |
| 43 | + mkdirSync(join(dir, '.tmp')) | ||
| 44 | + writeFileSync(join(dir, '.tmp', 'erp-lane-db'), laneMarker) | ||
| 45 | + } | ||
| 46 | + const childEnv = { ...process.env, ...env } | ||
| 47 | + if (!('ERP_TEST_DB_SCHEMA' in env)) delete childEnv.ERP_TEST_DB_SCHEMA | ||
| 48 | + return spawnSync('node', [join(dir, 'scripts', 'seed-demo-data.mjs')], { encoding: 'utf8', env: childEnv }) | ||
| 39 | } | 49 | } |
| 40 | 50 | ||
| 41 | test('seed-demo-data: unfilled 【人工填写】 config placeholders fail before mysql is invoked', () => { | 51 | test('seed-demo-data: unfilled 【人工填写】 config placeholders fail before mysql is invoked', () => { |
| @@ -114,3 +124,27 @@ test('seed-demo-data: valid seed + port 1 passes local checks then fails at DB c | @@ -114,3 +124,27 @@ test('seed-demo-data: valid seed + port 1 passes local checks then fails at DB c | ||
| 114 | assert.doesNotMatch(r.stderr, /仍是占位/, 'stderr: ' + r.stderr) | 124 | assert.doesNotMatch(r.stderr, /仍是占位/, 'stderr: ' + r.stderr) |
| 115 | assert.doesNotMatch(r.stderr, /非法种子文件名/, 'stderr: ' + r.stderr) | 125 | assert.doesNotMatch(r.stderr, /非法种子文件名/, 'stderr: ' + r.stderr) |
| 116 | }) | 126 | }) |
| 127 | + | ||
| 128 | +// ── 测试 DB lane 化:取值顺序 env > .tmp/erp-lane-db marker > config ── | ||
| 129 | + | ||
| 130 | +const LANE_SEED = { '01__inventory.sql': '-- expect: inventory_item=3\nSELECT 1;\n' } | ||
| 131 | + | ||
| 132 | +test('seed-demo-data: ERP_TEST_DB_SCHEMA env overrides config schema', () => { | ||
| 133 | + const r = run({ seedFiles: LANE_SEED, env: { ERP_TEST_DB_SCHEMA: 'erp_dev_lane2' } }) | ||
| 134 | + assert.match(r.stdout, /目标库 erp_dev_lane2/, 'stdout: ' + r.stdout) | ||
| 135 | +}) | ||
| 136 | + | ||
| 137 | +test('seed-demo-data: .tmp/erp-lane-db marker overrides config when env is absent, with audit log', () => { | ||
| 138 | + const r = run({ seedFiles: LANE_SEED, laneMarker: 'erp_dev_lane3\n' }) | ||
| 139 | + assert.match(r.stdout, /lane DB marker 生效: erp_dev_lane3/, 'stdout: ' + r.stdout) | ||
| 140 | + assert.match(r.stdout, /目标库 erp_dev_lane3/, 'stdout: ' + r.stdout) | ||
| 141 | +}) | ||
| 142 | + | ||
| 143 | +test('seed-demo-data: env wins over marker; blank marker falls through to config', () => { | ||
| 144 | + const both = run({ seedFiles: LANE_SEED, env: { ERP_TEST_DB_SCHEMA: 'erp_dev_lane2' }, laneMarker: 'erp_dev_lane3\n' }) | ||
| 145 | + assert.match(both.stdout, /目标库 erp_dev_lane2/, 'stdout: ' + both.stdout) | ||
| 146 | + assert.doesNotMatch(both.stdout, /lane DB marker 生效/, 'stdout: ' + both.stdout) | ||
| 147 | + const blank = run({ seedFiles: LANE_SEED, laneMarker: '\n' }) | ||
| 148 | + assert.match(blank.stdout, /目标库 erp_dev/, 'stdout: ' + blank.stdout) | ||
| 149 | + assert.doesNotMatch(blank.stdout, /lane DB marker 生效/, 'stdout: ' + blank.stdout) | ||
| 150 | +}) |
lib/setup-test-db-template.test.mjs
| @@ -15,7 +15,9 @@ function runWithSchema(schemaLine) { | @@ -15,7 +15,9 @@ function runWithSchema(schemaLine) { | ||
| 15 | return runWithDb({ schemaLine }) | 15 | return runWithDb({ schemaLine }) |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | -function runWithDb({ host = '127.0.0.1', port = '1', user = 'root', password = 'x', schemaLine = 'schema: erp_dev' }) { | 18 | +// env:传给子进程的增量环境变量;laneMarker:非 null 时写 .tmp/erp-lane-db 兜底标记。 |
| 19 | +// 基础环境强制剔除 ERP_TEST_DB_SCHEMA——外层 shell 若恰好设了该变量,"缺省行为"用例会被污染。 | ||
| 20 | +function runWithDb({ host = '127.0.0.1', port = '1', user = 'root', password = 'x', schemaLine = 'schema: erp_dev', env = {}, laneMarker = null }) { | ||
| 19 | const dir = mkdtempSync(join(tmpdir(), 'erp-stdb-')) | 21 | const dir = mkdtempSync(join(tmpdir(), 'erp-stdb-')) |
| 20 | mkdirSync(join(dir, 'scripts')) | 22 | mkdirSync(join(dir, 'scripts')) |
| 21 | copyFileSync(TEMPLATE, join(dir, 'scripts', 'setup-test-db.mjs')) | 23 | copyFileSync(TEMPLATE, join(dir, 'scripts', 'setup-test-db.mjs')) |
| @@ -23,7 +25,13 @@ function runWithDb({ host = '127.0.0.1', port = '1', user = 'root', password = ' | @@ -23,7 +25,13 @@ function runWithDb({ host = '127.0.0.1', port = '1', user = 'root', password = ' | ||
| 23 | join(dir, 'config-vars.yaml'), | 25 | join(dir, 'config-vars.yaml'), |
| 24 | ['database:', ` host: ${host}`, ` port: ${port}`, ` user: ${user}`, ` password: ${password}`, ' ' + schemaLine, ''].join('\n'), | 26 | ['database:', ` host: ${host}`, ` port: ${port}`, ` user: ${user}`, ` password: ${password}`, ' ' + schemaLine, ''].join('\n'), |
| 25 | ) | 27 | ) |
| 26 | - return spawnSync('node', [join(dir, 'scripts', 'setup-test-db.mjs')], { encoding: 'utf8' }) | 28 | + if (laneMarker !== null) { |
| 29 | + mkdirSync(join(dir, '.tmp')) | ||
| 30 | + writeFileSync(join(dir, '.tmp', 'erp-lane-db'), laneMarker) | ||
| 31 | + } | ||
| 32 | + const childEnv = { ...process.env, ...env } | ||
| 33 | + if (!('ERP_TEST_DB_SCHEMA' in env)) delete childEnv.ERP_TEST_DB_SCHEMA | ||
| 34 | + return spawnSync('node', [join(dir, 'scripts', 'setup-test-db.mjs')], { encoding: 'utf8', env: childEnv }) | ||
| 27 | } | 35 | } |
| 28 | 36 | ||
| 29 | test('setup-test-db: empty schema fails before mysql is invoked', () => { | 37 | test('setup-test-db: empty schema fails before mysql is invoked', () => { |
| @@ -61,3 +69,33 @@ test('setup-test-db: non-test names are allowed in sandbox/dev workflows', () => | @@ -61,3 +69,33 @@ test('setup-test-db: non-test names are allowed in sandbox/dev workflows', () => | ||
| 61 | const r = runWithSchema('schema: erp_prod') | 69 | const r = runWithSchema('schema: erp_prod') |
| 62 | assert.match(r.stdout, /DROP \+ CREATE `erp_prod`/, 'stdout: ' + r.stdout) | 70 | assert.match(r.stdout, /DROP \+ CREATE `erp_prod`/, 'stdout: ' + r.stdout) |
| 63 | }) | 71 | }) |
| 72 | + | ||
| 73 | +// ── 测试 DB lane 化:取值顺序 env > .tmp/erp-lane-db marker > config ── | ||
| 74 | + | ||
| 75 | +test('setup-test-db: ERP_TEST_DB_SCHEMA env overrides config schema', () => { | ||
| 76 | + const r = runWithDb({ env: { ERP_TEST_DB_SCHEMA: 'erp_dev_lane2' } }) | ||
| 77 | + assert.match(r.stdout, /DROP \+ CREATE `erp_dev_lane2`/, 'stdout: ' + r.stdout) | ||
| 78 | +}) | ||
| 79 | + | ||
| 80 | +test('setup-test-db: empty ERP_TEST_DB_SCHEMA env falls through to config schema', () => { | ||
| 81 | + const r = runWithDb({ env: { ERP_TEST_DB_SCHEMA: '' } }) | ||
| 82 | + assert.match(r.stdout, /DROP \+ CREATE `erp_dev`/, 'stdout: ' + r.stdout) | ||
| 83 | +}) | ||
| 84 | + | ||
| 85 | +test('setup-test-db: .tmp/erp-lane-db marker overrides config when env is absent, with audit log', () => { | ||
| 86 | + const r = runWithDb({ laneMarker: 'erp_dev_lane3\n' }) | ||
| 87 | + assert.match(r.stdout, /lane DB marker 生效: erp_dev_lane3/, 'stdout: ' + r.stdout) | ||
| 88 | + assert.match(r.stdout, /DROP \+ CREATE `erp_dev_lane3`/, 'stdout: ' + r.stdout) | ||
| 89 | +}) | ||
| 90 | + | ||
| 91 | +test('setup-test-db: env wins over marker (marker log absent)', () => { | ||
| 92 | + const r = runWithDb({ env: { ERP_TEST_DB_SCHEMA: 'erp_dev_lane2' }, laneMarker: 'erp_dev_lane3\n' }) | ||
| 93 | + assert.match(r.stdout, /DROP \+ CREATE `erp_dev_lane2`/, 'stdout: ' + r.stdout) | ||
| 94 | + assert.doesNotMatch(r.stdout, /lane DB marker 生效/, 'stdout: ' + r.stdout) | ||
| 95 | +}) | ||
| 96 | + | ||
| 97 | +test('setup-test-db: blank marker falls through to config schema', () => { | ||
| 98 | + const r = runWithDb({ laneMarker: '\n' }) | ||
| 99 | + assert.match(r.stdout, /DROP \+ CREATE `erp_dev`/, 'stdout: ' + r.stdout) | ||
| 100 | + assert.doesNotMatch(r.stdout, /lane DB marker 生效/, 'stdout: ' + r.stdout) | ||
| 101 | +}) |
lib/test-template.test.mjs
0 → 100644
| 1 | +// lib/test-template.test.mjs — 校验生成模板 scripts/test.mjs 的测试 DB lane 化贯通逻辑。 | ||
| 2 | +// D5 的要害是 env 必须真正打穿到后端测试子进程(gradle fork / e2e globalSetup→bootRun 均靠继承 env, | ||
| 3 | +// 由 application*.yml 的 ${ERP_TEST_DB_SCHEMA:...} 占位消费)——故用探针命令替换 {{...}} 占位 | ||
| 4 | +// (模拟 skeleton-gen 的 Plan 期填充),直接断言子进程眼中的 ERP_TEST_DB_SCHEMA。 | ||
| 5 | +import { test } from 'node:test' | ||
| 6 | +import assert from 'node:assert/strict' | ||
| 7 | +import { spawnSync } from 'node:child_process' | ||
| 8 | +import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' | ||
| 9 | +import { tmpdir } from 'node:os' | ||
| 10 | +import { join } from 'node:path' | ||
| 11 | +import { fileURLToPath } from 'node:url' | ||
| 12 | + | ||
| 13 | +const TEMPLATE = fileURLToPath(new URL('../skills/plan/skeleton-gen/templates/scripts-test-template.mjs', import.meta.url)) | ||
| 14 | + | ||
| 15 | +// 探针脚本:以子进程身份打印自己继承到的 ERP_TEST_DB_SCHEMA(unset 显式可断言)。 | ||
| 16 | +// 占位填充值不能带引号(模板里占位嵌在 JS 单引号字符串内),故用脚本文件而非内联 node -e。 | ||
| 17 | +const PROBE_SOURCE = `console.log('PROBE_SCHEMA=' + (process.env.ERP_TEST_DB_SCHEMA ?? '(unset)'))\n` | ||
| 18 | + | ||
| 19 | +// env:传给闸门进程的增量环境变量;laneMarker:非 null 时写 .tmp/erp-lane-db 兜底标记。 | ||
| 20 | +// 基础环境强制剔除 ERP_TEST_DB_SCHEMA——外层 shell 若恰好设了该变量,"缺省行为"用例会被污染。 | ||
| 21 | +function runGate({ env = {}, laneMarker = null } = {}) { | ||
| 22 | + const dir = mkdtempSync(join(tmpdir(), 'erp-gate-')) | ||
| 23 | + mkdirSync(join(dir, 'scripts')) | ||
| 24 | + mkdirSync(join(dir, 'backend')) | ||
| 25 | + // backend_* 在 backend/ 子目录下执行、e2e 在仓库根执行,相对路径相应不同(frontend_* 因无 frontend/ 被跳过)。 | ||
| 26 | + const filled = readFileSync(TEMPLATE, 'utf8') | ||
| 27 | + .replace(/\{\{(backend|frontend)_[a-z_]+\}\}/g, 'node ../scripts/probe.mjs') | ||
| 28 | + .replace(/\{\{e2e_cmd\}\}/g, 'node scripts/probe.mjs') | ||
| 29 | + writeFileSync(join(dir, 'scripts', 'test.mjs'), filled) | ||
| 30 | + writeFileSync(join(dir, 'scripts', 'probe.mjs'), PROBE_SOURCE) | ||
| 31 | + // setup-test-db stub:真模板会连库失败中断闸门、且自身也读 marker,无法隔离 test.mjs 的回填路径。 | ||
| 32 | + writeFileSync(join(dir, 'scripts', 'setup-test-db.mjs'), PROBE_SOURCE) | ||
| 33 | + if (laneMarker !== null) { | ||
| 34 | + mkdirSync(join(dir, '.tmp')) | ||
| 35 | + writeFileSync(join(dir, '.tmp', 'erp-lane-db'), laneMarker) | ||
| 36 | + } | ||
| 37 | + const childEnv = { ...process.env, ...env } | ||
| 38 | + if (!('ERP_TEST_DB_SCHEMA' in env)) delete childEnv.ERP_TEST_DB_SCHEMA | ||
| 39 | + return spawnSync('node', [join(dir, 'scripts', 'test.mjs')], { encoding: 'utf8', env: childEnv }) | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +test('test.mjs: ERP_TEST_DB_SCHEMA env reaches every child process', () => { | ||
| 43 | + const r = runGate({ env: { ERP_TEST_DB_SCHEMA: 'erp_dev_lane2' } }) | ||
| 44 | + assert.equal(r.status, 0, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr) | ||
| 45 | + // setup-db / build / lint / unit / e2e 五步各打印一次。 | ||
| 46 | + assert.equal((r.stdout.match(/PROBE_SCHEMA=erp_dev_lane2/g) || []).length, 5, 'stdout: ' + r.stdout) | ||
| 47 | + assert.match(r.stdout, /GREEN/, 'stdout: ' + r.stdout) | ||
| 48 | +}) | ||
| 49 | + | ||
| 50 | +test('test.mjs: .tmp/erp-lane-db marker backfills env for children when env is absent', () => { | ||
| 51 | + const r = runGate({ laneMarker: 'erp_dev_lane3\n' }) | ||
| 52 | + assert.equal(r.status, 0, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr) | ||
| 53 | + assert.match(r.stdout, /lane DB marker 生效: ERP_TEST_DB_SCHEMA=erp_dev_lane3/, 'stdout: ' + r.stdout) | ||
| 54 | + assert.equal((r.stdout.match(/PROBE_SCHEMA=erp_dev_lane3/g) || []).length, 5, 'stdout: ' + r.stdout) | ||
| 55 | +}) | ||
| 56 | + | ||
| 57 | +test('test.mjs: env wins over marker (marker log absent)', () => { | ||
| 58 | + const r = runGate({ env: { ERP_TEST_DB_SCHEMA: 'erp_dev_lane2' }, laneMarker: 'erp_dev_lane3\n' }) | ||
| 59 | + assert.equal(r.status, 0, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr) | ||
| 60 | + assert.equal((r.stdout.match(/PROBE_SCHEMA=erp_dev_lane2/g) || []).length, 5, 'stdout: ' + r.stdout) | ||
| 61 | + assert.doesNotMatch(r.stdout, /lane DB marker 生效/, 'stdout: ' + r.stdout) | ||
| 62 | +}) | ||
| 63 | + | ||
| 64 | +test('test.mjs: no env and no marker leaves children unset (serial behavior unchanged)', () => { | ||
| 65 | + const r = runGate() | ||
| 66 | + assert.equal(r.status, 0, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr) | ||
| 67 | + assert.equal((r.stdout.match(/PROBE_SCHEMA=\(unset\)/g) || []).length, 5, 'stdout: ' + r.stdout) | ||
| 68 | + assert.doesNotMatch(r.stdout, /lane DB marker 生效/, 'stdout: ' + r.stdout) | ||
| 69 | +}) |
skills/plan/skeleton-gen/templates/docs-04-skeleton-template.md
| @@ -3,6 +3,10 @@ | @@ -3,6 +3,10 @@ | ||
| 3 | ### 1.1 规则 | 3 | ### 1.1 规则 |
| 4 | - 【人工填写:一条后端通用约定,按需复制本行新增更多;无则填「无」】 | 4 | - 【人工填写:一条后端通用约定,按需复制本行新增更多;无则填「无」】 |
| 5 | 5 | ||
| 6 | +**测试库 env 贯通(锁定约定,生成时原样保留)**: | ||
| 7 | +- 后端 `application*.yml`(含测试 profile)JDBC URL 的 schema 段一律写成 Spring 占位 `${ERP_TEST_DB_SCHEMA:<config-vars.yaml database.schema 字面量>}`(如 `jdbc:mysql://localhost:3306/${ERP_TEST_DB_SCHEMA:erp_dev}?...`)。env 缺省回退原 schema、串行行为不变;并行 lane 经该 env 把整条测试/起栈链指向 lane 库——gradle test fork 的 JVM 与 bootRun 默认继承 env,无需拼 `SPRING_DATASOURCE_URL`。 | ||
| 8 | +- Flyway 配置 `out-of-order: true`:并行版本段下版本号**只保唯一、不保连续、不保合并序**;独立模块的 migration 互不引用(正是同波次并行的准入条件),乱序 apply 安全。 | ||
| 9 | + | ||
| 6 | ### 1.2 分层结构 | 10 | ### 1.2 分层结构 |
| 7 | 11 | ||
| 8 | ### 1.3 命名约定 | 12 | ### 1.3 命名约定 |
skills/plan/skeleton-gen/templates/scripts-seed-demo-data-template.mjs
| @@ -86,11 +86,26 @@ if (!existsSync(CONFIG_FILE)) { | @@ -86,11 +86,26 @@ if (!existsSync(CONFIG_FILE)) { | ||
| 86 | 86 | ||
| 87 | const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {} | 87 | const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {} |
| 88 | 88 | ||
| 89 | +// lane 兜底 marker:并行波次执行器建好 lane worktree 后在仓库根写 .tmp/erp-lane-db(一行 lane schema 名)。 | ||
| 90 | +// 与 scripts/setup-test-db.mjs 同一约定:取值顺序 env > marker > config——提示词链丢前缀时 | ||
| 91 | +// 种子仍确定性灌进 lane 库,不污染共享库;主根无 marker 且 env 缺省时走 config,串行行为一字不变。 | ||
| 92 | +function readLaneDbMarker() { | ||
| 93 | + const markerFile = join(PROJECT_ROOT, '.tmp', 'erp-lane-db') | ||
| 94 | + if (!existsSync(markerFile)) return '' | ||
| 95 | + return readFileSync(markerFile, 'utf8').split('\n')[0].trim() | ||
| 96 | +} | ||
| 97 | + | ||
| 89 | const DB_HOST = db.host ?? '' | 98 | const DB_HOST = db.host ?? '' |
| 90 | const DB_PORT = db.port ?? '3306' | 99 | const DB_PORT = db.port ?? '3306' |
| 91 | const DB_USER = db.user ?? '' | 100 | const DB_USER = db.user ?? '' |
| 92 | const DB_PASSWORD = db.password ?? '' | 101 | const DB_PASSWORD = db.password ?? '' |
| 93 | -const DB_SCHEMA = db.schema ?? '' | 102 | +// 校验逻辑沿用,作用于生效值。 |
| 103 | +const LANE_MARKER_SCHEMA = readLaneDbMarker() | ||
| 104 | +const DB_SCHEMA = process.env.ERP_TEST_DB_SCHEMA || LANE_MARKER_SCHEMA || (db.schema ?? '') | ||
| 105 | +// marker 是不可见的兜底路径(env 前缀在命令行上自明),生效时必须留痕便于排查指错库。 | ||
| 106 | +if (!process.env.ERP_TEST_DB_SCHEMA && LANE_MARKER_SCHEMA) { | ||
| 107 | + console.log(`${LOG} lane DB marker 生效: ${LANE_MARKER_SCHEMA}`) | ||
| 108 | +} | ||
| 94 | 109 | ||
| 95 | function rejectPlaceholder(key, value) { | 110 | function rejectPlaceholder(key, value) { |
| 96 | if (typeof value === 'string' && value.includes('【人工填写')) { | 111 | if (typeof value === 'string' && value.includes('【人工填写')) { |
skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs
| @@ -61,11 +61,26 @@ if (!existsSync(CONFIG_FILE)) { | @@ -61,11 +61,26 @@ if (!existsSync(CONFIG_FILE)) { | ||
| 61 | 61 | ||
| 62 | const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {} | 62 | const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {} |
| 63 | 63 | ||
| 64 | +// lane 兜底 marker:并行波次执行器建好 lane worktree 后在仓库根写 .tmp/erp-lane-db(一行 lane schema 名)。 | ||
| 65 | +// lane 注入是 prompt-only(提示词链两跳,任何一跳丢 ERP_TEST_DB_SCHEMA 前缀即对共享库 DROP+CREATE), | ||
| 66 | +// 本脚本是最具毁灭性的操作,必须有确定性兜底;主根无 marker 且 env 缺省时走 config,串行行为一字不变。 | ||
| 67 | +function readLaneDbMarker() { | ||
| 68 | + const markerFile = join(SCRIPT_DIR, '..', '.tmp', 'erp-lane-db') | ||
| 69 | + if (!existsSync(markerFile)) return '' | ||
| 70 | + return readFileSync(markerFile, 'utf8').split('\n')[0].trim() | ||
| 71 | +} | ||
| 72 | + | ||
| 64 | const DB_HOST = db.host ?? '' | 73 | const DB_HOST = db.host ?? '' |
| 65 | const DB_PORT = db.port ?? '3306' | 74 | const DB_PORT = db.port ?? '3306' |
| 66 | const DB_USER = db.user ?? '' | 75 | const DB_USER = db.user ?? '' |
| 67 | const DB_PASSWORD = db.password ?? '' | 76 | const DB_PASSWORD = db.password ?? '' |
| 68 | -const DB_SCHEMA = db.schema ?? '' | 77 | +// 取值顺序 env > marker > config(并行 lane 化;校验逻辑沿用,作用于生效值)。 |
| 78 | +const LANE_MARKER_SCHEMA = readLaneDbMarker() | ||
| 79 | +const DB_SCHEMA = process.env.ERP_TEST_DB_SCHEMA || LANE_MARKER_SCHEMA || (db.schema ?? '') | ||
| 80 | +// marker 是不可见的兜底路径(env 前缀在命令行上自明),生效时必须留痕便于排查指错库。 | ||
| 81 | +if (!process.env.ERP_TEST_DB_SCHEMA && LANE_MARKER_SCHEMA) { | ||
| 82 | + console.log(`[setup-test-db] lane DB marker 生效: ${LANE_MARKER_SCHEMA}`) | ||
| 83 | +} | ||
| 69 | 84 | ||
| 70 | function rejectPlaceholder(key, value) { | 85 | function rejectPlaceholder(key, value) { |
| 71 | if (typeof value === 'string' && value.includes('【人工填写')) { | 86 | if (typeof value === 'string' && value.includes('【人工填写')) { |
skills/plan/skeleton-gen/templates/scripts-test-template.mjs
| @@ -9,12 +9,28 @@ | @@ -9,12 +9,28 @@ | ||
| 9 | // 命令字符串来自 docs/04 §零(构建/lint/单测/e2e)——由 skeleton-gen 在 Plan 期填充。 | 9 | // 命令字符串来自 docs/04 §零(构建/lint/单测/e2e)——由 skeleton-gen 在 Plan 期填充。 |
| 10 | 10 | ||
| 11 | import { spawnSync } from 'node:child_process' | 11 | import { spawnSync } from 'node:child_process' |
| 12 | -import { existsSync } from 'node:fs' | 12 | +import { existsSync, readFileSync } from 'node:fs' |
| 13 | import { dirname, join } from 'node:path' | 13 | import { dirname, join } from 'node:path' |
| 14 | import { fileURLToPath } from 'node:url' | 14 | import { fileURLToPath } from 'node:url' |
| 15 | 15 | ||
| 16 | const PROJECT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') | 16 | const PROJECT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') |
| 17 | 17 | ||
| 18 | +// 测试 DB lane 化:本闸门自身不连库,schema 经 ERP_TEST_DB_SCHEMA env 贯通到全部子进程—— | ||
| 19 | +// setup-test-db.mjs 直读该 env;gradle test fork 的 JVM 与 e2e globalSetup 起的 bootRun 默认继承 env, | ||
| 20 | +// 由后端 application*.yml 的 ${ERP_TEST_DB_SCHEMA:<缺省 schema>} 占位消费(锁定约定见 docs/04 §一)。 | ||
| 21 | +// env 缺省时读 lane 兜底 marker(.tmp/erp-lane-db,并行波次执行器建 lane worktree 后写入)回填 process.env, | ||
| 22 | +// 保证提示词链丢前缀时整个闸门仍确定性指向 lane 库;主根无 marker,串行行为一字不变。 | ||
| 23 | +if (!process.env.ERP_TEST_DB_SCHEMA) { | ||
| 24 | + const markerFile = join(PROJECT_ROOT, '.tmp', 'erp-lane-db') | ||
| 25 | + if (existsSync(markerFile)) { | ||
| 26 | + const laneSchema = readFileSync(markerFile, 'utf8').split('\n')[0].trim() | ||
| 27 | + if (laneSchema !== '') { | ||
| 28 | + process.env.ERP_TEST_DB_SCHEMA = laneSchema | ||
| 29 | + console.log(`[test.mjs] lane DB marker 生效: ERP_TEST_DB_SCHEMA=${laneSchema}`) | ||
| 30 | + } | ||
| 31 | + } | ||
| 32 | +} | ||
| 33 | + | ||
| 18 | // 在指定子目录下跑一条 shell 命令;非零退出码即终止整个闸门并透传该码。 | 34 | // 在指定子目录下跑一条 shell 命令;非零退出码即终止整个闸门并透传该码。 |
| 19 | function run(label, command, cwd = PROJECT_ROOT) { | 35 | function run(label, command, cwd = PROJECT_ROOT) { |
| 20 | console.log(`[test.mjs] ${label}: ${command}`) | 36 | console.log(`[test.mjs] ${label}: ${command}`) |