Skip to content

refactor(process): main process decoupling phase 2 — full DI for all bridge/service modules#1447

Merged
kaizhou-lab merged 10 commits intomainfrom
zynx/refactor/main-process-decouple-phase2
Mar 19, 2026
Merged

refactor(process): main process decoupling phase 2 — full DI for all bridge/service modules#1447
kaizhou-lab merged 10 commits intomainfrom
zynx/refactor/main-process-decouple-phase2

Conversation

@piorpua
Copy link
Copy Markdown
Contributor

@piorpua piorpua commented Mar 19, 2026

Summary

Completes Phase 2 of the main process decoupling plan. All remaining bridge and service modules now receive their dependencies via constructor injection instead of importing singletons directly. Every new business-logic class can be unit-tested without Electron, SQLite, or any singleton.

PRs in this branch (6 commits)

Commit Scope Change
PR-A WorkerTaskManager Inject IConversationRepository; remove direct getDatabase() + ProcessChat calls
PR-B Bridge layer Inject IWorkerTaskManager into taskBridge, geminiConversationBridge, acpConversationBridge, applicationBridge
PR-C CronService Full decoupling via ICronRepository, ICronEventEmitter, ICronJobExecutor; remove all 6 singleton imports
PR-D Channel repository Introduce IChannelRepository + SqliteChannelRepository; inject into channelBridge
PR-E databaseBridge + extensionsBridge Inject IConversationRepository; extract ActivitySnapshotBuilder as testable class
PR-F Cleanup Delete old conversationService.ts; migrate ActionExecutor + SystemActions to IConversationService; add Phase 2 files to coverage

New interfaces

  • IChannelRepository
  • ICronRepository
  • ICronEventEmitter
  • ICronJobExecutor

New implementations

  • SqliteChannelRepository, SqliteCronRepository, IpcCronEventEmitter, WorkerTaskManagerJobExecutor
  • FallbackConversationRepository (decorator over IConversationRepository)
  • ActivitySnapshotBuilder (extracted from extensionsBridge)
  • conversationServiceSingleton (replaces old static ConversationService)

New tests (15 new test files)

WorkerTaskManager, conversationBridge, taskBridge, geminiConversationBridge, acpConversationBridge, applicationBridge, channelBridge, databaseBridge, extensionsBridge (ActivitySnapshotBuilder), CronService (expanded)

Test plan

  • bun run test — 844 tests pass
  • bunx tsc --noEmit — no new TypeScript errors
  • bun run lint:fix — no errors (warnings only)
  • bun run test:coverage — verify new files ≥ 80%
  • bun run test:integration — integration regression gate

zynx added 7 commits March 19, 2026 12:27
… (Phase 2 PR-A)

- Add `listAllConversations()` to IConversationRepository and IConversationService interfaces
- Implement `listAllConversations()` in SqliteConversationRepository and ConversationServiceImpl
- Add FallbackConversationRepository decorator delegating all methods to inner repo
- Refactor WorkerTaskManager constructor to accept IConversationRepository;
  remove direct getDatabase() and ProcessChat calls from getOrBuildTask
- Wire SqliteConversationRepository into workerTaskManagerSingleton
- Fix conversationBridge.getAssociateConversation to call conversationService.listAllConversations()
  instead of getDatabase() — closes last direct DB coupling in conversationBridge
- Rewrite WorkerTaskManager.test.ts to use mock IConversationRepository
- Add conversationBridge.test.ts covering listAllConversations path and failure cases
…s (Phase 2 PR-B)

- taskBridge, geminiConversationBridge, acpConversationBridge, applicationBridge
  now accept workerTaskManager: IWorkerTaskManager as a parameter instead of
  importing the singleton directly
- bridge/index.ts passes deps.workerTaskManager to each init function
- Add unit tests: taskBridge, geminiConversationBridge, acpConversationBridge,
  applicationBridge — covering both success and failure paths
…injection (PR-C)

- Add ICronRepository, ICronEventEmitter, ICronJobExecutor interfaces
- Add SqliteCronRepository, IpcCronEventEmitter, WorkerTaskManagerJobExecutor implementations
- Refactor CronService to accept constructor injection; remove singleton export
- Add cronServiceSingleton.ts for production wiring
- Export CronBusyGuard class (previously unexported)
- Update all cronService import sites to use cronServiceSingleton
- Rewrite CronService.test.ts to use mock interfaces (no SQLite, no Electron required)
…oupling (PR-D)

- Add IChannelRepository interface with getChannelPlugins, getPendingPairingRequests,
  getChannelUsers, deleteChannelUser, getChannelSessions
- Add SqliteChannelRepository delegating to getDatabase() channel methods
- Refactor initChannelBridge() to accept IChannelRepository instead of calling
  getDatabase() directly; remove unused row-converter imports
- Wire SqliteChannelRepository into initAllBridges via BridgeDependencies
- Add channelBridge.test.ts with 11 unit tests covering all DB-touching handlers
  and failure paths (repo throws → error response)
…and extensionsBridge (PR-E)

- Add searchMessages() and order param to getMessages() in IConversationRepository
- Implement searchMessages() in SqliteConversationRepository and FallbackConversationRepository
- Refactor databaseBridge.ts: replace getDatabase() with injected IConversationRepository
- Extract ActivitySnapshotBuilder class (src/process/bridge/services/) with injected repo + taskManager
- Refactor extensionsBridge.ts: remove singleton imports, accept repo + taskManager params
- Add conversationRepo to BridgeDependencies and pass through initAllBridges
- Add 16 unit tests for databaseBridge (all 3 handlers + failure paths)
- Add 6 unit tests for ActivitySnapshotBuilder (totalConversations, runningConversations, grouping, error state)
…nService singleton

- Create conversationServiceSingleton.ts wired with SqliteConversationRepository
- Migrate SystemActions.ts: replace ConversationService static methods with
  conversationServiceSingleton.createConversation; adapt result shape from
  {success, conversation?, error?} to try/catch around Promise<TChatConversation>
- Migrate ActionExecutor.ts: same replacement; inline conversation creation now
  uses try/catch instead of success-flag branching
- Delete src/process/services/conversationService.ts (old singleton with direct
  getDatabase() and workerTaskManager calls)
- Update vitest.config.ts coverage.include to add all Phase 2 new files:
  FallbackConversationRepository, SqliteChannelRepository, SqliteCronRepository,
  IpcCronEventEmitter, WorkerTaskManagerJobExecutor, ActivitySnapshotBuilder,
  databaseBridge
…ontracts

Shows the 10 new interface contracts, implementation classes, singleton
wiring points, and constructor-injected bridges introduced in Phase 2.
@piorpua
Copy link
Copy Markdown
Contributor Author

piorpua commented Mar 19, 2026

Code Review:refactor(process): main process decoupling phase 2 — full DI for all bridge/service modules (#1447)

变更概述

本 PR 完成主进程解耦第二阶段,将所有 bridge 和 service 模块从直接导入单例改为构造函数注入。新增了 IChannelRepositoryICronRepositoryICronEventEmitterICronJobExecutor 四个接口及对应实现;CronService、各 bridge 函数均改为接收注入依赖;conversationService.ts 被删除,相关消费者迁移到 IConversationService。同时新增 15 个测试文件覆盖改动模块。


方案评估

结论⚠️ 方案有缺陷

整体 DI 方向正确,接口划分清晰,单元测试可独立运行的目标基本达到。但 CronService.runJob() 的重构存在一处行为回归:setProcessing(true) 的调用时机被提前,导致 executor 抛出时会泄漏 "busy" 状态。此外 FallbackConversationRepository 是无行为的空壳类,既无实例化点也无测试,应删除或补充实际 fallback 逻辑。


问题清单

🟠 HIGH — CronService.runJob():setProcessing(true) 调用时机提前,异常路径导致 "busy" 状态永久泄漏

文件src/process/services/cron/CronService.ts,第 385 行

问题代码

// Mark conversation as busy BEFORE registering the idle callback
this.executor.setProcessing(conversationId, true);
this.registerCompletionNotification(job);

// Delegate task-get + sendMessage to the executor.
await this.executor.executeJob(job);

问题说明

原始代码中,cronBusyGuard.setProcessing(true) 只在成功获取 task 之后才调用——若 getOrBuildTask 抛出(会话不存在),代码提前 returnsetProcessing 不会被执行。

重构后,setProcessing(true)无条件提前,在 executeJob() 之前调用。WorkerTaskManagerJobExecutor.executeJob() 内部调用 getOrBuildTask,若会话不存在则抛出异常,异常向上传播到 runJob() 的 catch 块。catch 块更新了 job 状态,但从不调用 setProcessing(false),导致该会话被永久标记为 "busy"。

setProcessing(false) 由各 AgentManager(GeminiAgentManagerAcpAgentManagerCodexAgentManager 等)在 sendMessage 完成后调用。若 executeJob 抛出,agent 从未创建,没有任何代码会调用 setProcessing(false)

后续触发同一 job 时,isConversationBusy() 返回 true,触发重试逻辑;达到 maxRetries 后标记为 "skipped",但 busy 状态依然存在,该 job 彻底失效直到下次 App 重启(cleanupOrphanJobs 清理)。

触发条件:ConversationServiceImpl.deleteConversation() 在清理 cron job 时若抛出异常,被外层 try-catch 静默吞掉,会话已删但 cron job 残留;下次 job 触发时即命中此 bug。

修复建议:将 setProcessing(true) 移入 WorkerTaskManagerJobExecutor.executeJob() 内部,仅在确认 task 存在(sendMessage 之前)设置:

// WorkerTaskManagerJobExecutor.executeJob()
async executeJob(job: CronJob): Promise<void> {
  const { conversationId } = job.metadata;
  // ... acquire task (may throw — no setProcessing called yet) ...

  // Only mark busy after task is successfully acquired
  this.busyGuard.setProcessing(conversationId, true);

  // ... sendMessage ...
}

同时从 CronService.executeJob() 中删除提前的 this.executor.setProcessing(conversationId, true) 调用(registerCompletionNotification 须在 setProcessing(true) 之前调用,以避免 onceIdle 竞态,这一顺序需在 executor 内部保持)。


🟡 MEDIUM — FallbackConversationRepository 无实际逻辑、无实例化点、无测试,是死代码

文件src/process/database/FallbackConversationRepository.ts

问题代码

export class FallbackConversationRepository implements IConversationRepository {
  constructor(private readonly db: IConversationRepository) {}

  getConversation(id: string): TChatConversation | undefined {
    return this.db.getConversation(id);   // pure delegation, no fallback
  }
  // ... all 9 methods are identical pass-throughs
}

问题说明:类注释明确写道 "ProcessChat file-storage fallback is handled via lazy migration in databaseBridge, not here",意味着该类当前没有任何 fallback 行为——所有方法只是委托给内部 repo。在整个 diff 中,没有任何地方实例化此类,且没有对应的测试文件(vitest.config.ts 虽将其加入 coverage,但实际覆盖率为 0%)。

修复建议:若此类是未来扩展的占位符,需在 PR 中补充测试并有至少一个实例化点;若 fallback 逻辑已由 databaseBridge 处理,则删除此类并从 vitest.config.tscoverage.include 中移除。


🔵 LOW — WorkerTaskManager.test.tsmakeRepo() 缺少 searchMessages mock

文件tests/unit/WorkerTaskManager.test.ts

问题代码

function makeRepo(overrides?: Partial<IConversationRepository>): IConversationRepository {
  return {
    getConversation: vi.fn(),
    createConversation: vi.fn(),
    updateConversation: vi.fn(),
    deleteConversation: vi.fn(),
    getMessages: vi.fn(),
    insertMessage: vi.fn(),
    getUserConversations: vi.fn(),
    listAllConversations: vi.fn(() => []),
    // searchMessages 缺失 ❌
    ...overrides,
  };
}

问题说明IConversationRepository 现在包含 searchMessages 方法,但 makeRepo 未实现该方法。其他测试文件(databaseBridge.test.tsextensionsBridge.test.ts)中的同类工厂函数均已包含。

修复建议

searchMessages: vi.fn(() => ({ items: [], total: 0, page: 0, pageSize: 20, hasMore: false })),

汇总

# 严重级别 文件 问题
1 🟠 HIGH src/process/services/cron/CronService.ts:385 setProcessing(true)executeJob 前调用,异常时 busy 状态泄漏
2 🟡 MEDIUM src/process/database/FallbackConversationRepository.ts 无行为、无实例化、无测试的死代码类
3 🔵 LOW tests/unit/WorkerTaskManager.test.ts makeRepo() 缺少 searchMessages mock

结论

需要修改#1(HIGH)是行为回归,会在会话被删除后导致定时任务 busy 状态永久泄漏;#2 引入了误导性的空壳类。两个问题都应在合并前修复。


本报告由本地 pr-review skill 生成,包含完整项目上下文,无截断限制。

zynx added 2 commits March 19, 2026 17:12
- Fix setProcessing(true) busy-state leak: move into executeJob after task
  acquisition via onAcquired callback, preserving onceIdle ordering guarantee
- Remove FallbackConversationRepository dead-code class (no instantiation
  point, no fallback logic, no tests); remove from vitest coverage include
- Add missing searchMessages mock to WorkerTaskManager.test.ts makeRepo()

Review follow-up for #1447
@piorpua
Copy link
Copy Markdown
Contributor Author

piorpua commented Mar 19, 2026

PR Fix 验证报告

原始 PR: #1447
修复提交: 492a2f5

# 严重级别 文件 问题 修复方式 状态
1 🟠 HIGH src/process/services/cron/CronService.ts:385 setProcessing(true) 提前调用,异常时 busy 状态永久泄漏 setProcessing(true) 移入 WorkerTaskManagerJobExecutor.executeJob() 任务获取成功后执行;通过 onAcquired 回调保持 onceIdle 注册顺序 ✅ 已修复
2 🟡 MEDIUM src/process/database/FallbackConversationRepository.ts 无行为、无实例化、无测试的死代码类 删除文件;从 vitest.config.ts coverage include 中移除 ✅ 已修复
3 🔵 LOW tests/unit/WorkerTaskManager.test.ts makeRepo() 缺少 searchMessages mock 添加 searchMessages: vi.fn(...) ✅ 已修复

总结: ✅ 已修复 3 个 | ❌ 未能修复 0 个 | ⏭️ 跳过 0 个

质量门控: bun run lint:fix ✅ · bun run format ✅ · bunx tsc --noEmit ✅ · bun run test ✅(903 个测试全部通过)

…rview

Class was dead code with no instantiation point or fallback logic;
architecture diagram updated to match actual implementation.
@kaizhou-lab kaizhou-lab merged commit ec68a4b into main Mar 19, 2026
14 of 17 checks passed
kaizhou-lab pushed a commit that referenced this pull request Mar 19, 2026
- Fix setProcessing(true) busy-state leak: move into executeJob after task
  acquisition via onAcquired callback, preserving onceIdle ordering guarantee
- Remove FallbackConversationRepository dead-code class (no instantiation
  point, no fallback logic, no tests); remove from vitest coverage include
- Add missing searchMessages mock to WorkerTaskManager.test.ts makeRepo()

Review follow-up for #1447
@piorpua piorpua deleted the zynx/refactor/main-process-decouple-phase2 branch March 19, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants