seed-demo-data-template.test.mjs
5.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// lib/seed-demo-data-template.test.mjs — 校验生成模板 scripts/seed-demo-data.mjs 的演示种子注入逻辑。
// 跑的是真实模板产物:复制到临时 scripts/ 下、写一个 ../config-vars.yaml、可选写 sql/seed/*.sql、再 node 执行。
// 所有会触达 DB 的用例 host/port 故意指向 127.0.0.1:1(必拒连),不会触碰真实库。
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
const TEMPLATE = fileURLToPath(new URL('../skills/plan/skeleton-gen/templates/scripts-seed-demo-data-template.mjs', import.meta.url))
// 搭一个临时目标项目:scripts/seed-demo-data.mjs + config-vars.yaml + 可选 sql/seed/*.sql,然后跑脚本。
// seedFiles 为 null 表示不创建 sql/seed 目录;为对象 { name: content } 表示创建目录并写入这些文件({} = 空目录)。
function run({
host = '127.0.0.1',
port = '1',
user = 'root',
password = 'x',
schemaLine = 'schema: erp_dev',
seedFiles = null,
} = {}) {
const dir = mkdtempSync(join(tmpdir(), 'erp-seed-'))
mkdirSync(join(dir, 'scripts'))
copyFileSync(TEMPLATE, join(dir, 'scripts', 'seed-demo-data.mjs'))
writeFileSync(
join(dir, 'config-vars.yaml'),
['database:', ` host: ${host}`, ` port: ${port}`, ` user: ${user}`, ` password: ${password}`, ' ' + schemaLine, ''].join('\n'),
)
if (seedFiles !== null) {
const seedDir = join(dir, 'sql', 'seed')
mkdirSync(seedDir, { recursive: true })
for (const [name, content] of Object.entries(seedFiles)) {
writeFileSync(join(seedDir, name), content)
}
}
return spawnSync('node', [join(dir, 'scripts', 'seed-demo-data.mjs')], { encoding: 'utf8' })
}
test('seed-demo-data: unfilled 【人工填写】 config placeholders fail before mysql is invoked', () => {
for (const cfg of [
{ host: '【人工填写:MySQL host】' },
{ port: '【人工填写:MySQL port】' },
{ user: '【人工填写:账号】' },
{ password: '【人工填写:密码】' },
{ schemaLine: 'schema: 【人工填写:schema 名】' },
]) {
const r = run(cfg)
assert.equal(r.status, 1, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
assert.match(r.stderr, /仍是占位/, 'stderr: ' + r.stderr)
}
})
test('seed-demo-data: empty schema fails before mysql is invoked', () => {
const r = run({ schemaLine: 'schema:' })
assert.equal(r.status, 1)
assert.match(r.stderr, /database\.schema/, '应是 schema 缺失报错而非连库失败 — stderr: ' + r.stderr)
})
test('seed-demo-data: missing sql/seed directory exits 0 with no-seed message', () => {
const r = run({ seedFiles: null })
assert.equal(r.status, 0, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
assert.match(r.stdout, /无种子文件/, 'stdout: ' + r.stdout)
})
test('seed-demo-data: empty sql/seed directory exits 0 with no-seed message', () => {
const r = run({ seedFiles: {} })
assert.equal(r.status, 0, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
assert.match(r.stdout, /无种子文件/, 'stdout: ' + r.stdout)
})
test('seed-demo-data: illegal seed filename fails before mysql is invoked', () => {
const r = run({ seedFiles: { '01 bad.sql': '-- expect: t=0\n' } })
assert.equal(r.status, 1, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
assert.match(r.stderr, /非法种子文件名/, 'stderr: ' + r.stderr)
assert.match(r.stderr, /01 bad\.sql/, '应列出违规文件名 — stderr: ' + r.stderr)
// 文件名校验在任何 mysql 调用之前 —— 不应出现已进入 DB 阶段的日志。
assert.doesNotMatch(r.stdout, /目标库/, 'stdout: ' + r.stdout)
})
// 缺 <NN>__ 结构的文件名(会被旧宽松正则放过,但违反 <NN>__<module_id>.sql 契约)必须被拒。
test('seed-demo-data: filenames missing the <NN>__ structure are rejected', () => {
for (const bad of [
'inventory.sql', // 无 NN__ 前缀
'1__x.sql', // 单位数 NN
'001__x.sql', // 三位数 NN
'01-x.sql', // 连字符而非双下划线
'01_x.sql', // 单下划线而非双下划线
'01__.sql', // module_id 为空
'01__bad-id.sql', // module_id 含连字符(非 [A-Za-z0-9_])
]) {
const r = run({ seedFiles: { [bad]: '-- expect: t=0\n' } })
assert.equal(r.status, 1, `${bad} 应被拒 — stdout: ${r.stdout} stderr: ${r.stderr}`)
assert.match(r.stderr, /非法种子文件名/, `${bad} — stderr: ${r.stderr}`)
// 文件名校验在任何 mysql 调用之前 —— 不应进入 DB 阶段。
assert.doesNotMatch(r.stdout, /目标库/, `${bad} — stdout: ${r.stdout}`)
}
})
// 合法 <NN>__<module_id>.sql 通过文件名校验后进入 DB 阶段(验证收紧后的正则不误伤合法名)。
test('seed-demo-data: well-formed <NN>__<module_id>.sql passes filename check', () => {
const r = run({ seedFiles: { '01__inventory.sql': '-- expect: inventory_item=3\nSELECT 1;\n' } })
assert.equal(r.status, 1, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
assert.doesNotMatch(r.stderr, /非法种子文件名/, 'stderr: ' + r.stderr)
assert.match(r.stdout, /目标库 erp_dev/, '应已进入 DB 阶段 — stdout: ' + r.stdout)
})
test('seed-demo-data: valid seed + port 1 passes local checks then fails at DB connect', () => {
const r = run({ seedFiles: { '01__inventory.sql': '-- expect: inventory_item=3\nSELECT 1;\n' } })
assert.equal(r.status, 1, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
// 已通过全部本地校验、进入 DB 阶段:应打印目标库摘要,且报错是连库失败(非占位/非法名)。
assert.match(r.stdout, /目标库 erp_dev/, '应已进入 DB 阶段 — stdout: ' + r.stdout)
assert.doesNotMatch(r.stderr, /仍是占位/, 'stderr: ' + r.stderr)
assert.doesNotMatch(r.stderr, /非法种子文件名/, 'stderr: ' + r.stderr)
})