Commit 4a0c400253c42fd0d25f1038a9dc7fd0e76f8ad5

Authored by zichun
1 parent af4ac730

skeleton-gen: 测试库 lane 化——ERP_TEST_DB_SCHEMA env 覆盖 + marker 兜底链 + application*.…

…yml 占位约定 + Flyway out-of-order;env/marker 双缺省行为一字不变
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}`)