feat(round2+round3): 完整实现 A/B/C/D 主线 + round3-F/H 修复
Round2 主线:
- A: 事件落库地基 (RuntimeApp EventStore 单例 + 14 repo wiring)
- B: 执行体对齐 (read-before-edit, verification-before-completion)
- C: 界面对齐 (@opentui/solid, 删除 runtime 依赖)
- D: 经验闭环 (ExperienceMiner, DebuggerRole, CompactorRole)
Round2 补充修复:
- fail-on-missing 反作弊门禁
- projection-store-apply.test.ts 补写
- 3个空壳测试转行为 (evidence-store, recovery-impl, knowledge-store)
- ask 项目根支持 AIRCODING_PROJECT_ROOT
- Worker 事件契约修复 (task.attempt.started → checkpoint)
Round3-F: cpp 工具切换
- 删除 BuiltInToolRegistrar cpp.* 闭包
- 接入 toolchain-cpp 真实 CppToolRegistrar
- canonical envelope {status/output/metadata}
- ExecutorRole system prompt 对齐新工具名
Round3-H: Doctor 5 类报告
- toolchain (cmake/ninja/cppcheck/clangd/g++)
- display (X11/Wayland + ImageMagick)
- network (internet connectivity)
- provider (api_key/base_url/model/connectivity)
Secret 脱敏:
- 状态交接.md: sk- → \${OPENAI_API_KEY}
- .gitignore: 添加 .air/ .claude/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,55 +1,95 @@
|
||||
/**
|
||||
* Regression test: Recovery implementation completeness
|
||||
*
|
||||
* Verifies that checkPidLiveness and scanOrphanReferences have real
|
||||
* implementations, not just stub return values.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { readFileSync } from 'fs'
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { Database } from 'bun:sqlite'
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
const SOURCE_PATH = join(
|
||||
import.meta.dir,
|
||||
'..',
|
||||
'..',
|
||||
'src',
|
||||
'storage',
|
||||
'Recovery.ts'
|
||||
)
|
||||
|
||||
const source = readFileSync(SOURCE_PATH, 'utf-8')
|
||||
import { Recovery } from '../../src/storage/Recovery.js'
|
||||
|
||||
describe('Recovery implementation', () => {
|
||||
test('checkPidLiveness is not a stub (has implementation code)', () => {
|
||||
// Should have actual implementation with loop logic
|
||||
expect(source).toContain('for (const agent of agents)')
|
||||
expect(source).toContain("action: alive ? 'keep' : 'mark_lost'")
|
||||
// Should have more than just a bare return []
|
||||
expect(source).toContain('const reports: PidLivenessReport[] = []')
|
||||
const created: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of created.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('checkPidLiveness uses process.kill for liveness check', () => {
|
||||
// Should use process.kill(pid, 0) for signal-0 liveness check
|
||||
expect(source).toContain('process.kill(agent.pid, 0)')
|
||||
function makeRecovery(): { recovery: Recovery; root: string; artifactRoot: string; dbPath: string } {
|
||||
const root = mkdtempSync(join(tmpdir(), 'air-recovery-'))
|
||||
created.push(root)
|
||||
const artifactRoot = join(root, 'artifacts')
|
||||
const dbPath = join(root, 'session.db')
|
||||
const db = new Database(dbPath)
|
||||
db.exec(`
|
||||
CREATE TABLE sessions (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE tasks (id TEXT PRIMARY KEY, session_id TEXT);
|
||||
CREATE TABLE messages (id TEXT PRIMARY KEY, session_id TEXT);
|
||||
CREATE TABLE task_attempts (id TEXT PRIMARY KEY, task_id TEXT);
|
||||
CREATE TABLE agents (id TEXT PRIMARY KEY, session_id TEXT);
|
||||
CREATE TABLE tool_runs (id TEXT PRIMARY KEY, session_id TEXT);
|
||||
CREATE TABLE command_runs (id TEXT PRIMARY KEY, session_id TEXT);
|
||||
CREATE TABLE artifacts (id TEXT PRIMARY KEY, session_id TEXT);
|
||||
CREATE TABLE evidence_refs (evidence_ref_id TEXT PRIMARY KEY, session_id TEXT);
|
||||
INSERT INTO tasks (id, session_id) VALUES ('task_orphan', 'missing_session');
|
||||
INSERT INTO task_attempts (id, task_id) VALUES ('attempt_orphan', 'missing_task');
|
||||
`)
|
||||
db.close()
|
||||
return {
|
||||
recovery: new Recovery({
|
||||
sessionId: 'session_recovery' as any,
|
||||
projectId: 'project_recovery' as any,
|
||||
artifactRoot,
|
||||
dbPath,
|
||||
projectRoot: root,
|
||||
}),
|
||||
root,
|
||||
artifactRoot,
|
||||
dbPath,
|
||||
}
|
||||
}
|
||||
|
||||
test('checks PID liveness with keep/mark_lost actions', () => {
|
||||
const { recovery } = makeRecovery()
|
||||
const reports = recovery.checkPidLiveness([
|
||||
{ agent_id: 'self', pid: process.pid },
|
||||
{ agent_id: 'missing', pid: 99999999 },
|
||||
])
|
||||
recovery.close()
|
||||
|
||||
expect(reports).toEqual([
|
||||
{ agent_id: 'self', pid: process.pid, alive: true, action: 'keep' },
|
||||
{ agent_id: 'missing', pid: 99999999, alive: false, action: 'mark_lost' },
|
||||
])
|
||||
})
|
||||
|
||||
test('scanOrphanReferences returns OrphanReferenceReport structure', () => {
|
||||
// Should define fkChecks array with the 8 invariant checks
|
||||
expect(source).toContain('fkChecks')
|
||||
expect(source).toContain("table: 'tasks'")
|
||||
expect(source).toContain("table: 'messages'")
|
||||
expect(source).toContain("table: 'task_attempts'")
|
||||
expect(source).toContain("table: 'agents'")
|
||||
expect(source).toContain("table: 'tool_runs'")
|
||||
expect(source).toContain("table: 'command_runs'")
|
||||
expect(source).toContain("table: 'artifacts'")
|
||||
expect(source).toContain("table: 'evidence_refs'")
|
||||
test('scans orphan references from SQLite tables', async () => {
|
||||
const { recovery } = makeRecovery()
|
||||
const report = await recovery.scan()
|
||||
recovery.close()
|
||||
|
||||
// Should iterate over checks
|
||||
expect(source).toContain('for (const check of fkChecks)')
|
||||
expect(report.orphanReferences.totalFound).toBeGreaterThanOrEqual(2)
|
||||
expect(report.orphanReferences.archived).toContainEqual({
|
||||
table: 'tasks',
|
||||
id: 'missing_session',
|
||||
reason: 'FK-off: session_id → sessions (1 rows)',
|
||||
})
|
||||
expect(report.orphanReferences.archived).toContainEqual({
|
||||
table: 'task_attempts',
|
||||
id: 'missing_task',
|
||||
reason: 'FK-off: task_id → tasks (1 rows)',
|
||||
})
|
||||
})
|
||||
|
||||
// Should return a proper report
|
||||
expect(source).toContain('return report')
|
||||
test('quarantines non-artifact temporary orphan files', async () => {
|
||||
const { recovery, artifactRoot } = makeRecovery()
|
||||
const tmpDir = join(artifactRoot, 'tmp')
|
||||
const orphanPath = join(tmpDir, 'scratch.tmp')
|
||||
await Bun.write(orphanPath, 'orphan')
|
||||
|
||||
const report = await recovery.scan()
|
||||
recovery.close()
|
||||
|
||||
expect(report.orphanArtifacts.totalFound).toBe(1)
|
||||
expect(report.orphanArtifacts.quarantined).toHaveLength(1)
|
||||
expect(existsSync(report.orphanArtifacts.quarantined[0])).toBe(true)
|
||||
expect(existsSync(orphanPath)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user