diff --git a/src/store/db.ts b/src/store/db.ts index 76a004c..7be1c14 100644 --- a/src/store/db.ts +++ b/src/store/db.ts @@ -37,6 +37,11 @@ export function openDb(file: string): Database.Database { ensureColumn(db, 'projects', 'budget_usd', 'budget_usd REAL'); // 项目当期预算上限 USD(null=不限) ensureColumn(db, 'projects', 'budget_period', "budget_period TEXT NOT NULL DEFAULT 'month'"); // 预算周期 day|month ensureColumn(db, 'tasks', 'attachments', 'attachments TEXT'); // 附件 JSON [{name,type,path}](图片/文件随任务提交) + // checks/auto_approve_* 在 schema.sql 的 CREATE TABLE 里有,但既有 DB 表已存在时 CREATE 是 no-op, + // 故这三列必须也走 ensureColumn 迁移(否则旧 DB 缺列,createProject INSERT 报错)。 + ensureColumn(db, 'projects', 'checks', 'checks TEXT'); // 分项检查命令 JSON {"lint":..,"typecheck":..,"build":..} + ensureColumn(db, 'projects', 'auto_approve_plan', 'auto_approve_plan INTEGER NOT NULL DEFAULT 0'); // 拆解全 easy 时跳过 plan_review + ensureColumn(db, 'projects', 'auto_approve_exec', 'auto_approve_exec INTEGER NOT NULL DEFAULT 0'); // 双复审 approve 后跳过 exec_review 自动合并 ensureColumn(db, 'projects', 'agent_rules', 'agent_rules TEXT'); // L2 项目级 agent 执行规范(注入 worker prompt) const schema = readFileSync(join(HERE, 'schema.sql'), 'utf8'); db.exec(schema); diff --git a/test/db-migration.test.ts b/test/db-migration.test.ts new file mode 100644 index 0000000..cca9a92 --- /dev/null +++ b/test/db-migration.test.ts @@ -0,0 +1,51 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import Database from 'better-sqlite3'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { openDb } from '../src/store/db.js'; +import { Store } from '../src/store/index.js'; + +/** + * 回归:旧 DB 的 projects 表早于 checks、auto_approve_x、budget、agent_rules 等列创建, + * schema.sql 的 CREATE TABLE IF NOT EXISTS 对已存在的表是 no-op → 这些列必须靠 ensureColumn 迁移补上。 + * 漏迁移会导致 createProject 的 INSERT 引用不存在的列而报错(线上实测复现)。 + */ +test('迁移:旧 projects 表缺新列 → openDb 补齐 + createProject 不报错', () => { + const dir = mkdtempSync(join(tmpdir(), 'maestro-mig-')); + const file = join(dir, 'old.db'); + try { + // 造一个"旧版" projects 表(只有最早期那批列,缺 checks/auto_approve_*/budget/agent_rules…) + const raw = new Database(file); + raw.exec(`CREATE TABLE projects ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, repo_path TEXT NOT NULL UNIQUE, + default_branch TEXT NOT NULL DEFAULT 'main', verify_cmd TEXT, + autonomy TEXT NOT NULL DEFAULT 'manual', model TEXT, + concurrency INTEGER NOT NULL DEFAULT 1, status TEXT NOT NULL DEFAULT 'active', + created_at TEXT NOT NULL + )`); + raw.close(); + + // openDb 跑迁移:应补齐所有后加列 + const db = openDb(file); + const cols = (db.prepare(`PRAGMA table_info(projects)`).all() as Array<{ name: string }>).map((c) => c.name); + for (const c of [ + 'checks', 'auto_approve_plan', 'auto_approve_exec', 'agent_rules', + 'budget_usd', 'budget_period', 'max_retries', 'timeout_ms', 'last_sync_at', 'logo', 'sort_order', + ]) { + assert.ok(cols.includes(c), `应补列 ${c}`); + } + db.close(); + + // Store 在该旧库上建项目(INSERT 引用 checks/auto_approve_* 列)应成功 + const s = new Store(file); + const p = s.createProject({ name: 'mig', repoPath: '/tmp/mig-' + Math.random() }); + assert.equal(p.autoApprovePlan, false); + assert.equal(p.autoApproveExec, false); + assert.equal(p.checks, null); + s.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +});