Files
maestro/test/db-migration.test.ts
T
wangjia 59ce391ecc feat(models+gate): 模型按项目可配置(默认 opus-4.8) + diff 声明外文件硬闸
① 模型可配置(项目维度,默认 opus-4.8)
- 新增 projects.models(JSON,按角色 executor/planner/reviewer/conflict
  覆盖,值可为字符串=全复杂度统一 或 {easy,medium,hard} 分档)
- models.ts 重构 resolveModel:优先级 项目级 models > 旧 project.model
  (仅 executor/planner) > env > 默认 DEFAULT_MODEL(opus-4.8)
- 取消内置 fable/sonnet 分档默认:所有角色默认 opus-4.8(彻底回避 fable-5
  不可用问题,需要时项目级显式配置即可);回退链改 opus→sonnet→fable
- API PATCH /projects 透传 models;sanitizeModels 落库校验

② diff 声明外文件闸(task.scopeFiles)
- 新增 tasks.scope_files(JSON glob/路径数组)
- checks.ts: globToRegExp/matchesAnyGlob + scopeFileGate(改动文件越界=硬闸,
  空声明跳过,git 出错不拦截);pipeline runApproveGates 接入
- planner 拆解新增每子任务 files 字段:prompt 要求 + parseDecompose 解析 +
  ingest 落 scopeFiles,自动填充声明范围
- executor prompt 注入「声明文件范围约束」,让 agent 知边界(gate 才公平)

迁移:projects.models / tasks.scope_files 走 ensureColumn 幂等迁移(旧库补列)
测试:models 默认/配置/优先级、scope glob/gate、planner files 解析、
      store 持久化往返、迁移补列 —— 237 通过

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 07:06:32 +08:00

55 lines
2.5 KiB
TypeScript

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', 'models',
'budget_usd', 'budget_period', 'max_retries', 'timeout_ms', 'last_sync_at', 'logo', 'sort_order',
]) {
assert.ok(cols.includes(c), `应补列 ${c}`);
}
// tasks.scope_files 也走 ensureColumn 迁移
const taskCols = (db.prepare(`PRAGMA table_info(tasks)`).all() as Array<{ name: string }>).map((c) => c.name);
assert.ok(taskCols.includes('scope_files'), '应补 tasks.scope_files 列');
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 });
}
});