feat(开发脚本): dev:all 支持 --clear-database 并提示本地库 schema 冲突
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m18s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m19s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m55s
Project CI / Backend tests (pull_request) Successful in 9m11s
Project CI / Frontend tests (pull_request) Successful in 3m37s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m46s
Project CI / Native shell tests (pull_request) Successful in 8m35s
Project CI / Repository checks (pull_request) Successful in 7m12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m18s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m19s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m55s
Project CI / Backend tests (pull_request) Successful in 9m11s
Project CI / Frontend tests (pull_request) Successful in 3m37s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m46s
Project CI / Native shell tests (pull_request) Successful in 8m35s
Project CI / Repository checks (pull_request) Successful in 7m12s
默认仍写死 --preserve-database(不改默认行为);显式 --clear-database 时不再传它、逐字透传 dev.mjs 既有语义;根栈因 manual migration / Reordering table 失败时补一段可操作提示(打印库名与 data dir、两条出路、清库会丢数据的警告),提示文案抽成纯函数并补单测;文档补一句
This commit is contained in:
+66
-9
@@ -15,6 +15,14 @@ const agcDataDir = resolve(
|
||||
);
|
||||
const agcDataDirArgument = 'server-rs/.spacetimedb/ai-game-creator/data';
|
||||
const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json');
|
||||
// AGC 配套本地库的 schema 冲突识别:SpacetimeDB 拒绝自动迁移时会在服务端日志里留下这两类特征串
|
||||
// (`… needs manual migration` / `Reordering table <name> requires a manual migration`)。
|
||||
const localSchemaConflictPattern =
|
||||
/manual migration|Reordering table/i;
|
||||
// 唯一允许「冲突即清库」的显式开关:dev:all 默认写死 --preserve-database(不删库),
|
||||
// 只有调用方显式传 --clear-database(dev.mjs 既有语义:preserve=false → -c=on-conflict)才清库。
|
||||
const clearDatabaseOption = '--clear-database';
|
||||
const spacetimeStartLogName = 'logs/dev-spacetime-start.log';
|
||||
const rootReadyTimeoutMs = 660_000;
|
||||
const rootReadyPollMs = 500;
|
||||
const childGracefulTimeoutMs = 2_500;
|
||||
@@ -75,8 +83,47 @@ function validateDevAllArgs(argv = []) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 判定根栈失败是否由「本地库 schema 与源码不一致」造成(纯函数,供单测钉住)。
|
||||
export function isLocalSchemaConflictOutput(output = '') {
|
||||
return localSchemaConflictPattern.test(String(output ?? ''));
|
||||
}
|
||||
|
||||
/// 读 SpacetimeDB 启动日志尾部(失败诊断用):文件不存在或读取失败都返回空串,绝不抛。
|
||||
function readSpacetimeStartLogTail(dataDir = agcDataDir, maxBytes = 64 * 1024) {
|
||||
const logPath = resolve(dataDir, spacetimeStartLogName);
|
||||
if (!existsSync(logPath)) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const text = readFileSync(logPath, 'utf8');
|
||||
return text.length > maxBytes ? text.slice(-maxBytes) : text;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成可操作提示(纯函数):库名与 data dir 打实际值,给出两条出路,并明确清库会丢本地数据。
|
||||
export function buildLocalSchemaConflictHint({
|
||||
database = agcDatabase,
|
||||
dataDir = agcDataDirArgument,
|
||||
} = {}) {
|
||||
return [
|
||||
'[dev:all] 本地 SpacetimeDB schema 与源码不一致:本次 publish 需要手动迁移而被拒(不是代码问题)。',
|
||||
` 目标库:${database}`,
|
||||
` 数据目录:${dataDir}`,
|
||||
' 两条出路:',
|
||||
` ① 清库重建后重跑:npm run dev:all -- ${clearDatabaseOption}`,
|
||||
` ② 只想先起根栈自愈:npm run dev -- --database ${database} --spacetime-data-dir ${dataDir}(不带 --preserve-database)`,
|
||||
' ⚠ ① 与 ② 都会丢弃该本地库里的数据(仅本地开发数据,生产库不受影响)。',
|
||||
' ⚠ 默认不加开关时 dev:all 会保留该库,冲突不会自愈——这是有意为之,避免误清库。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildRootDevArgs(argv = []) {
|
||||
validateDevAllArgs(argv);
|
||||
// 默认继续写死 --preserve-database(不改默认,避免误清库);
|
||||
// 只有调用方显式传 --clear-database 时才交给根栈按 dev.mjs 既有语义(preserve=false → -c=on-conflict)清库。
|
||||
const wantsClearDatabase = argv.includes(clearDatabaseOption);
|
||||
return [
|
||||
'run',
|
||||
'dev',
|
||||
@@ -85,7 +132,7 @@ function buildRootDevArgs(argv = []) {
|
||||
agcDatabase,
|
||||
'--spacetime-data-dir',
|
||||
agcDataDirArgument,
|
||||
'--preserve-database',
|
||||
...(wantsClearDatabase ? [] : ['--preserve-database']),
|
||||
'--no-interactive',
|
||||
...argv,
|
||||
];
|
||||
@@ -538,14 +585,24 @@ async function runDevAll(argv = process.argv.slice(2), dependencies = {}) {
|
||||
);
|
||||
rootChild = spawnProcess(npmCommand, rootArgs, env);
|
||||
const rootExit = waitForChildExit(rootChild);
|
||||
const rootState = await waitForRoot(rootChild, {
|
||||
readState,
|
||||
expected: {
|
||||
expectedRepoRoot: repoRoot,
|
||||
expectedDatabase: agcDatabase,
|
||||
expectedDataDir: agcDataDir,
|
||||
},
|
||||
});
|
||||
let rootState;
|
||||
try {
|
||||
rootState = await waitForRoot(rootChild, {
|
||||
readState,
|
||||
expected: {
|
||||
expectedRepoRoot: repoRoot,
|
||||
expectedDatabase: agcDatabase,
|
||||
expectedDataDir: agcDataDir,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// 根栈失败原因常常只在 SpacetimeDB 启动日志里(子进程 stdio 是 inherit,dev:all 抓不到它的 stdout):
|
||||
// 命中「本地库 schema 需手动迁移」时补一段可操作提示,否则原样抛出。
|
||||
if (isLocalSchemaConflictOutput(readSpacetimeStartLogTail())) {
|
||||
console.error(buildLocalSchemaConflictHint());
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (rootChild.exitCode !== null || rootChild.signalCode !== null) {
|
||||
throw new Error('根开发栈已退出,不能启动 AGC');
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
agcDataDir,
|
||||
buildDevAllBaseEnvironment,
|
||||
buildDevAllEnvironment,
|
||||
buildLocalSchemaConflictHint,
|
||||
buildManagedSpawnOptions,
|
||||
buildRootDevArgs,
|
||||
buildStartupSummary,
|
||||
isHttpReady,
|
||||
isLocalSchemaConflictOutput,
|
||||
isSharedRootStackReady,
|
||||
renderStartupTable,
|
||||
runDevAll,
|
||||
@@ -249,4 +251,58 @@ describe('dev:all 参数和共享开发栈合同', () => {
|
||||
expect(children).toHaveLength(2);
|
||||
expect(stopCalls).toEqual([children[1], children[0]]);
|
||||
});
|
||||
|
||||
test('默认仍固定保留 AGC 本地库(不改变默认行为)', () => {
|
||||
const args = buildRootDevArgs(['--watch']);
|
||||
expect(args).toContain('--preserve-database');
|
||||
expect(args).not.toContain('--clear-database');
|
||||
});
|
||||
|
||||
test('显式 --clear-database 时不再固定 --preserve-database,开关逐字透传给根栈', () => {
|
||||
const args = buildRootDevArgs(['--clear-database']);
|
||||
expect(args).not.toContain('--preserve-database');
|
||||
expect(args.filter((argument) => argument === '--clear-database')).toHaveLength(
|
||||
1,
|
||||
);
|
||||
expect(args).toEqual([
|
||||
'run',
|
||||
'dev',
|
||||
'--',
|
||||
'--database',
|
||||
agcDatabase,
|
||||
'--spacetime-data-dir',
|
||||
'server-rs/.spacetimedb/ai-game-creator/data',
|
||||
'--no-interactive',
|
||||
'--clear-database',
|
||||
]);
|
||||
});
|
||||
|
||||
test('识别本地库 schema 冲突的服务端输出', () => {
|
||||
expect(isLocalSchemaConflictOutput('… needs manual migration')).toBe(true);
|
||||
expect(
|
||||
isLocalSchemaConflictOutput(
|
||||
'Reordering table game_distribution_game requires a manual migration',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isLocalSchemaConflictOutput(
|
||||
'Aborting publish due to required manual migration.',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isLocalSchemaConflictOutput('Finished `dev` profile [unoptimized]')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isLocalSchemaConflictOutput(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
test('本地库 schema 冲突提示含库名/data dir/两条出路/丢数据警告', () => {
|
||||
const hint = buildLocalSchemaConflictHint();
|
||||
expect(hint).toContain(agcDatabase);
|
||||
expect(hint).toContain('server-rs/.spacetimedb/ai-game-creator/data');
|
||||
expect(hint).toContain('npm run dev:all -- --clear-database');
|
||||
expect(hint).toContain('不带 --preserve-database');
|
||||
expect(hint).toContain('都会丢弃该本地库里的数据');
|
||||
expect(hint).toContain('不是代码问题');
|
||||
expect(hint).not.toContain('undefined');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user