fix: integration test — 24/24 pass, all subsystems operational

DoctorService:
- check_bun(): search common bun paths (~/.bun/bin/bun, /usr/*)

RuntimeApp:
- Create DatabaseHandle adapter for Bun's raw Database
- Wire migrations through proper adapter (add query() method)

Scheduler:
- rebuild_from_db(): graceful no-op when tasks table doesn't exist yet

BuiltInToolRegistrar:
- All executors return {status:'ok', ...} for ToolRegistry.call()

Integration test results: 24/24 pass
- RuntimeApp creation: 5/5 subsystems
- Startup: DB init, migrations, tools, recovery
- Tools: 10 built-in tools verified
- Tool calls: fs.read/write/list/stat, project.scan
- Shutdown: clean close

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AirCoding
2026-06-05 13:19:53 +08:00
parent 1288a9b26c
commit 44c53422c9
4 changed files with 46 additions and 25 deletions

View File

@@ -103,8 +103,16 @@ export class RuntimeApp {
try { try {
const raw_db = this.db.getRawDatabase() const raw_db = this.db.getRawDatabase()
if (raw_db) { if (raw_db) {
// Build a DatabaseHandle adapter for Bun's Database
const dbHandle = {
id: 'startup',
db: raw_db,
query: (sql: string, ...params: unknown[]) =>
raw_db.prepare(sql).all(...params),
prepare: (sql: string) => raw_db.prepare(sql),
} as any
const runner = new MigrationRunner() const runner = new MigrationRunner()
await runner.migrate({ id: 'startup', db: raw_db } as any) await runner.migrate(dbHandle)
this.logger.info('Database migrations complete') this.logger.info('Database migrations complete')
} }
} catch (e: any) { } catch (e: any) {

View File

@@ -110,12 +110,20 @@ export class DoctorService {
} }
private check_bun(): DoctorCheck { private check_bun(): DoctorCheck {
// Search bun in common paths, not just PATH
const candidates = [
'bun', // PATH
`${process.env.HOME || '/root'}/.bun/bin/bun`,
'/usr/local/bin/bun',
'/usr/bin/bun',
]
for (const bun of candidates) {
try { try {
const version = execFileSync('bun', ['--version'], { stdio: 'pipe', timeout: 5000 }).toString().trim() const version = execFileSync(bun, ['--version'], { stdio: 'pipe', timeout: 5000 }).toString().trim()
return { name: 'bun', category: 'self_bootstrap', passed: true, message: `Bun ${version} found`, fixable: false } return { name: 'bun', category: 'self_bootstrap', passed: true, message: `Bun ${version} found`, fixable: false }
} catch { } catch { /* try next */ }
return { name: 'bun', category: 'self_bootstrap', passed: false, message: 'Bun not found', fixable: true, fix: 'Install Bun: curl -fsSL https://bun.sh/install | bash' }
} }
return { name: 'bun', category: 'self_bootstrap', passed: false, message: 'Bun not found', fixable: true, fix: 'Install Bun: curl -fsSL https://bun.sh/install | bash' }
} }
private check_sqlite(): DoctorCheck { private check_sqlite(): DoctorCheck {

View File

@@ -323,8 +323,13 @@ export class Scheduler {
rehydrated++ rehydrated++
} }
} catch (err) { } catch (err) {
// Table may not exist on first run (graceful degradation)
if (err && typeof err === 'object' && 'message' in err && String((err as any).message).includes('no such table')) {
// First run — no tasks table yet, this is expected
} else {
console.error('rebuild_from_db failed:', err) console.error('rebuild_from_db failed:', err)
} }
}
this.state = 'PLANNING_WAVE' this.state = 'PLANNING_WAVE'
return rehydrated return rehydrated

View File

@@ -177,7 +177,7 @@ export class BuiltInToolRegistrar {
try { try {
const { path } = call.arguments as { path: string } const { path } = call.arguments as { path: string }
const s = statSync(resolve(project_root, path)) const s = statSync(resolve(project_root, path))
return { call_id: call.call_id, tool_name: 'fs.stat', type: 'text', return { status: "ok", call_id: call.call_id, tool_name: 'fs.stat', type: 'text',
content: { path, exists: true, size: s.size, is_dir: s.isDirectory(), is_file: s.isFile(), content: { path, exists: true, size: s.size, is_dir: s.isDirectory(), is_file: s.isFile(),
mode: s.mode, mtime: s.mtime.toISOString(), ctime: s.ctime.toISOString() }, mode: s.mode, mtime: s.mtime.toISOString(), ctime: s.ctime.toISOString() },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name: 'fs.stat', type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name: 'fs.stat', type: 'ok' } }
@@ -191,7 +191,7 @@ export class BuiltInToolRegistrar {
try { try {
const { pid, signal = 'SIGTERM' } = call.arguments as { pid: number; signal?: string } const { pid, signal = 'SIGTERM' } = call.arguments as { pid: number; signal?: string }
process.kill(pid, signal as NodeJS.Signals) process.kill(pid, signal as NodeJS.Signals)
return { call_id: call.call_id, tool_name: 'process.kill', type: 'text', return { status: "ok", call_id: call.call_id, tool_name: 'process.kill', type: 'text',
content: { pid, signal, killed: true }, content: { pid, signal, killed: true },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name: 'process.kill', type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name: 'process.kill', type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -204,7 +204,7 @@ export class BuiltInToolRegistrar {
try { try {
const { path, base_ref = 'HEAD' } = call.arguments as { path: string; base_ref?: string } const { path, base_ref = 'HEAD' } = call.arguments as { path: string; base_ref?: string }
execFileSync('git', ['worktree', 'add', path, base_ref], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8' }) execFileSync('git', ['worktree', 'add', path, base_ref], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8' })
return { call_id: call.call_id, tool_name, type: 'text', content: { path, base_ref, created: true }, return { status: "ok", call_id: call.call_id, tool_name, type: 'text', content: { path, base_ref, created: true },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name },
@@ -216,7 +216,7 @@ export class BuiltInToolRegistrar {
try { try {
const { workspace_id } = call.arguments as { workspace_id: string; strategy?: string } const { workspace_id } = call.arguments as { workspace_id: string; strategy?: string }
execFileSync('git', ['merge', '--no-ff', workspace_id], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8' }) execFileSync('git', ['merge', '--no-ff', workspace_id], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8' })
return { call_id: call.call_id, tool_name, type: 'text', content: { workspace_id, merged: true }, return { status: "ok", call_id: call.call_id, tool_name, type: 'text', content: { workspace_id, merged: true },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name },
@@ -234,7 +234,7 @@ export class BuiltInToolRegistrar {
const ext = String(f).includes('.') ? (String(f).split('.').pop() || 'no_ext') : 'no_ext' const ext = String(f).includes('.') ? (String(f).split('.').pop() || 'no_ext') : 'no_ext'
by_ext[ext] = (by_ext[ext] || 0) + 1 by_ext[ext] = (by_ext[ext] || 0) + 1
} }
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { root: dir, total_files: entries.length, extensions: by_ext }, content: { root: dir, total_files: entries.length, extensions: by_ext },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -249,7 +249,7 @@ export class BuiltInToolRegistrar {
const dir = join(project_root, '.air', 'shared', 'profiles') const dir = join(project_root, '.air', 'shared', 'profiles')
if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
writeFileSync(join(dir, `${language}.json`), JSON.stringify(profile_json, null, 2)) writeFileSync(join(dir, `${language}.json`), JSON.stringify(profile_json, null, 2))
return { call_id: call.call_id, tool_name, type: 'text', content: { language, written: true }, return { status: "ok", call_id: call.call_id, tool_name, type: 'text', content: { language, written: true },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name },
@@ -262,7 +262,7 @@ export class BuiltInToolRegistrar {
const root = (call.arguments as any)?.project_root || project_root const root = (call.arguments as any)?.project_root || project_root
const cmake = existsSync(join(root, 'CMakeLists.txt')) const cmake = existsSync(join(root, 'CMakeLists.txt'))
const makefile = existsSync(join(root, 'Makefile')) const makefile = existsSync(join(root, 'Makefile'))
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { has_cmake: cmake, has_makefile: makefile, build_system: cmake ? 'cmake' : makefile ? 'make' : 'none' }, content: { has_cmake: cmake, has_makefile: makefile, build_system: cmake ? 'cmake' : makefile ? 'make' : 'none' },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -277,7 +277,7 @@ export class BuiltInToolRegistrar {
const buildDir = join(project_root, 'build') const buildDir = join(project_root, 'build')
if (!existsSync(buildDir)) mkdirSync(buildDir, { recursive: true }) if (!existsSync(buildDir)) mkdirSync(buildDir, { recursive: true })
execFileSync('cmake', ['-G', generator, '-DCMAKE_BUILD_TYPE=' + build_type, '..'], { cwd: buildDir, stdio: 'pipe' }) execFileSync('cmake', ['-G', generator, '-DCMAKE_BUILD_TYPE=' + build_type, '..'], { cwd: buildDir, stdio: 'pipe' })
return { call_id: call.call_id, tool_name, type: 'text', content: { generator, build_type, configured: true }, return { status: "ok", call_id: call.call_id, tool_name, type: 'text', content: { generator, build_type, configured: true },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name },
@@ -290,7 +290,7 @@ export class BuiltInToolRegistrar {
const { target, config = 'Debug' } = (call.arguments || {}) as any const { target, config = 'Debug' } = (call.arguments || {}) as any
const args = target ? ['--build', '.', '--config', config, '--target', target] : ['--build', '.', '--config', config] const args = target ? ['--build', '.', '--config', config, '--target', target] : ['--build', '.', '--config', config]
const out = execFileSync('cmake', args, { cwd: join(project_root, 'build'), stdio: 'pipe', timeout: 300000 }) const out = execFileSync('cmake', args, { cwd: join(project_root, 'build'), stdio: 'pipe', timeout: 300000 })
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { built: true, output: out.toString().slice(-500) }, content: { built: true, output: out.toString().slice(-500) },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -304,7 +304,7 @@ export class BuiltInToolRegistrar {
const { filter } = (call.arguments || {}) as any const { filter } = (call.arguments || {}) as any
const args = filter ? ['--output-on-failure', '-R', filter] : ['--output-on-failure'] const args = filter ? ['--output-on-failure', '-R', filter] : ['--output-on-failure']
const out = execFileSync('ctest', args, { cwd: join(project_root, 'build'), stdio: 'pipe', timeout: 300000 }) const out = execFileSync('ctest', args, { cwd: join(project_root, 'build'), stdio: 'pipe', timeout: 300000 })
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { passed: true, output: out.toString().slice(-1000) }, content: { passed: true, output: out.toString().slice(-1000) },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -317,7 +317,7 @@ export class BuiltInToolRegistrar {
try { try {
const { path = 'src' } = (call.arguments || {}) as any const { path = 'src' } = (call.arguments || {}) as any
const out = execFileSync('cppcheck', ['--enable=all', '--quiet', path], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8', timeout: 120000 }) const out = execFileSync('cppcheck', ['--enable=all', '--quiet', path], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8', timeout: 120000 })
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { output: out.toString().slice(-500), issues_found: 0 }, content: { output: out.toString().slice(-500), issues_found: 0 },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -330,7 +330,7 @@ export class BuiltInToolRegistrar {
try { try {
const { file, line = 0, column = 0 } = (call.arguments || {}) as any const { file, line = 0, column = 0 } = (call.arguments || {}) as any
const out = execFileSync('clangd', ['--check=' + file], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8', timeout: 30000 }) const out = execFileSync('clangd', ['--check=' + file], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8', timeout: 30000 })
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { file, line, column, diagnostics: out.toString().slice(-1000) }, content: { file, line, column, diagnostics: out.toString().slice(-1000) },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -343,7 +343,7 @@ export class BuiltInToolRegistrar {
try { try {
const { target } = (call.arguments || {}) as any const { target } = (call.arguments || {}) as any
const out = execFileSync('gdb', ['-batch', '-ex', 'run', '-ex', 'bt', '--', target], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8', timeout: 60000 }) const out = execFileSync('gdb', ['-batch', '-ex', 'run', '-ex', 'bt', '--', target], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8', timeout: 60000 })
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { target, backtrace: out.toString().slice(-2000) }, content: { target, backtrace: out.toString().slice(-2000) },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -357,7 +357,7 @@ export class BuiltInToolRegistrar {
const { log_path } = (call.arguments || {}) as any const { log_path } = (call.arguments || {}) as any
const content = readFileSync(resolve(project_root, log_path), 'utf-8') const content = readFileSync(resolve(project_root, log_path), 'utf-8')
const errors = content.split('\n').filter(l => /error|fail|segfault|assert|abort|exception/i.test(l)).slice(0, 50) const errors = content.split('\n').filter(l => /error|fail|segfault|assert|abort|exception/i.test(l)).slice(0, 50)
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { log_path, error_count: errors.length, errors }, content: { log_path, error_count: errors.length, errors },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -372,7 +372,7 @@ export class BuiltInToolRegistrar {
if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true }) if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true })
const tmpFile = join(tmpDir, `screenshot-${Date.now()}.png`) const tmpFile = join(tmpDir, `screenshot-${Date.now()}.png`)
execFileSync('import', ['-window', 'root', tmpFile], { stdio: 'pipe', timeout: 10000 }) execFileSync('import', ['-window', 'root', tmpFile], { stdio: 'pipe', timeout: 10000 })
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { captured: true, path: tmpFile }, content: { captured: true, path: tmpFile },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -387,7 +387,7 @@ export class BuiltInToolRegistrar {
const args = ['-i', iface, '-c', String(Math.min(Math.floor(duration_sec * 10), 50))] const args = ['-i', iface, '-c', String(Math.min(Math.floor(duration_sec * 10), 50))]
if (filter) args.push(filter) if (filter) args.push(filter)
const out = execFileSync('tcpdump', args, { stdio: 'pipe', timeout: (duration_sec + 5) * 1000 }) const out = execFileSync('tcpdump', args, { stdio: 'pipe', timeout: (duration_sec + 5) * 1000 })
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { interface: iface, duration_sec, packets: (String(out) || '').split('\n').length }, content: { interface: iface, duration_sec, packets: (String(out) || '').split('\n').length },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {
@@ -398,7 +398,7 @@ export class BuiltInToolRegistrar {
'permission.request': async (call: any) => { 'permission.request': async (call: any) => {
const { tool_name: tn, reason } = call.arguments as { tool_name: string; reason: string } const { tool_name: tn, reason } = call.arguments as { tool_name: string; reason: string }
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { tool_name: tn, reason, status: 'allowed', message: `Permission granted for ${tn}: ${reason}` }, content: { tool_name: tn, reason, status: 'allowed', message: `Permission granted for ${tn}: ${reason}` },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
}, },
@@ -415,7 +415,7 @@ export class BuiltInToolRegistrar {
const hasPkg = existsSync(join(project_root, 'package.json')) const hasPkg = existsSync(join(project_root, 'package.json'))
checks.push({ name: 'project_structure', passed: hasPkg, message: hasPkg ? 'Valid' : 'No package.json' }) checks.push({ name: 'project_structure', passed: hasPkg, message: hasPkg ? 'Valid' : 'No package.json' })
const allPassed = checks.every(c => c.passed) const allPassed = checks.every(c => c.passed)
return { call_id: call.call_id, tool_name, type: 'text', return { status: "ok", call_id: call.call_id, tool_name, type: 'text',
content: { checks, all_passed: allPassed, fixable_count: checks.filter(c => !c.passed).length }, content: { checks, all_passed: allPassed, fixable_count: checks.filter(c => !c.passed).length },
metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }
} catch (e: any) { } catch (e: any) {