diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f77ac03..41783da6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,13 @@ jobs: FAILS=$(grep -oE '[0-9]+ fail' /tmp/ci-test.log | tail -1 | grep -oE '[0-9]+') if [ -z "$FAILS" ]; then FAILS=0; fi echo "Failures: $FAILS (baseline: 0) | bun test exit: $TEST_EXIT" - # Show last 30 lines of test output + # Show failed tests first (they appear before the summary, which tail misses) + if [ "$FAILS" -gt 0 ]; then + echo "::group::Failed tests" + grep -E '\(fail\)' /tmp/ci-test.log || true + echo "::endgroup::" + fi + # Show last 30 lines for pass/fail summary tail -30 /tmp/ci-test.log if [ "$FAILS" -le 0 ]; then echo "✅ Test baseline OK" diff --git a/docs/BRANCH_EVENT_SOURCING.md b/docs/BRANCH_EVENT_SOURCING.md new file mode 100644 index 00000000..bf8def16 --- /dev/null +++ b/docs/BRANCH_EVENT_SOURCING.md @@ -0,0 +1,34 @@ +# 分支:persist/coordinator-event-sourcing + +## 存了什么 + +这是 CCP 采用 **Anthropic 原版事件溯源方案** 的快照——纯 JSONL 事件日志 + fold 投影,没有任何 SQLite 黑板代码。 + +## 核心文件 + +| 文件 | 作用 | +|------|------| +| `src/coordinator/teamEventStore.ts` | Event 类型定义 + EventStore 接口 + LocalFileEventStore(JSONL) | +| `src/coordinator/teamProjection.ts` | `applyEvent()` + `projectTeamState()` + `renderTeamContext()` | +| `src/coordinator/eventStoreInstance.ts` | 单例 + auto-switching(本地 JSONL / 远程 HTTP) | +| `src/coordinator/remoteEventStore.ts` | HTTP 客户端 EventStore(Phase 2 跨机器) | +| `src/coordinator/eventHttpServer.ts` | Bun.serve 事件服务器 | +| `src/coordinator/coordinatorMode.ts` | Coordinator mode 启动 + compaction hook(**不含黑板代码**) | +| `docs/Coordinator_Event_Log_设计文档.md` | 中文设计文档 | +| `docs/Coordinator_Event_Log_Design_Doc.md` | 英文设计文档 | +| `docs/plans/2026-06-11-coordinator-event-log.md` | 实施计划 | + +## 为什么不继续用 + +讨论结论(2026-06-12): +- 事件溯源在 CCP 的约束下是正确的——因为没有 SQLite +- 但它有维护成本:新事件类型需要改 fold、checkpoint 逻辑、1400 行代码 +- Hermes 的黑板(SQLite + structured keys)证明了更简单的方案可行 +- 最终决定:事件日志 + fold → SQLite 黑板(kv 表 + events 表同一事务) + +## 为什么保留 + +- 这是 Anthropic/CCB 的原生设计思路,有参考价值 +- 事件溯源的语义(有类型的事件、结构化聚合)比 pure last-write-wins 更丰富 +- 未来如果需要在 CCP 中做完整审计链路,事件表的 schema 可以从这里借鉴 +- 符合 CCP "保留上游设计痕迹" 的理念 diff --git a/packages/builtin-tools/src/tools/WebSearchTool/__tests__/adapterFactory.test.ts b/packages/builtin-tools/src/tools/WebSearchTool/__tests__/adapterFactory.test.ts index 4e5353d8..f44c852e 100644 --- a/packages/builtin-tools/src/tools/WebSearchTool/__tests__/adapterFactory.test.ts +++ b/packages/builtin-tools/src/tools/WebSearchTool/__tests__/adapterFactory.test.ts @@ -12,15 +12,25 @@ mock.module('src/utils/model/providers.js', () => ({ const { createAdapter } = await import('../adapters/index') const originalWebSearchAdapter = process.env.WEB_SEARCH_ADAPTER +const originalUseOpenAI = process.env.CLAUDE_CODE_USE_OPENAI +const originalUseGemini = process.env.CLAUDE_CODE_USE_GEMINI +const originalUseGrok = process.env.CLAUDE_CODE_USE_GROK afterEach(() => { isFirstPartyBaseUrl = true + // Restore WEB_SEARCH_ADAPTER if (originalWebSearchAdapter === undefined) { delete process.env.WEB_SEARCH_ADAPTER } else { process.env.WEB_SEARCH_ADAPTER = originalWebSearchAdapter } + + // Clear third-party provider flags so they don't hijack adapter selection + // (CI or local env may have CLAUDE_CODE_USE_OPENAI=1 set) + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_USE_GROK }) describe('createAdapter', () => { diff --git a/src/tasks/LocalAgentTask/__tests__/LocalAgentTask.test.ts b/src/tasks/LocalAgentTask/__tests__/LocalAgentTask.test.ts index 7300cbc9..f387c723 100644 --- a/src/tasks/LocalAgentTask/__tests__/LocalAgentTask.test.ts +++ b/src/tasks/LocalAgentTask/__tests__/LocalAgentTask.test.ts @@ -9,7 +9,23 @@ const noop = () => {} mock.module('src/utils/debug.ts', debugMock) mock.module('src/utils/log.ts', logMock) +const RealSessionStorage = await import('src/utils/sessionStorage.js') +const RealDiskOutput = await import('src/utils/task/diskOutput.js') +const RealMessageQueueManager = await import('src/utils/messageQueueManager.js') +const RealState = await import('src/bootstrap/state.js') +const RealSpeculation = await import( + 'src/services/PromptSuggestion/speculation.js' +) +const RealCleanupRegistry = await import('src/utils/cleanupRegistry.js') +const RealAbortController = await import('src/utils/abortController.js') +const RealSdkProgress = await import('src/utils/task/sdkProgress.js') +const RealSdkEventQueue = await import('src/utils/sdkEventQueue.js') +const RealXml = await import('src/constants/xml.js') +const RealAnalytics = await import('src/services/analytics/index.js') +const RealCollapseReadSearch = await import('src/utils/collapseReadSearch.js') + mock.module('src/utils/sessionStorage.js', () => ({ + ...RealSessionStorage, getAgentTranscriptPath: (id: string) => `/tmp/transcripts/${id}.jsonl`, recordSidechainTranscript: async () => {}, recordQueueOperation: noop, @@ -17,6 +33,7 @@ mock.module('src/utils/sessionStorage.js', () => ({ })) mock.module('src/utils/task/diskOutput.js', () => ({ + ...RealDiskOutput, evictTaskOutput: noop, getTaskOutputPath: (id: string) => `/tmp/output/${id}`, initTaskOutputAsSymlink: async () => {}, @@ -26,12 +43,14 @@ mock.module('src/utils/task/diskOutput.js', () => ({ // Capture enqueuePendingNotification calls for verification const enqueuedNotifications: string[] = [] mock.module('src/utils/messageQueueManager.js', () => ({ + ...RealMessageQueueManager, enqueuePendingNotification: (cmd: any) => { enqueuedNotifications.push(cmd.value) }, })) mock.module('src/bootstrap/state.js', () => ({ + ...RealState, getSdkAgentProgressSummariesEnabled: () => false, getSessionId: () => 'test-session-001', getProjectRoot: () => '/test/project', @@ -40,15 +59,18 @@ mock.module('src/bootstrap/state.js', () => ({ })) mock.module('src/services/PromptSuggestion/speculation.js', () => ({ + ...RealSpeculation, abortSpeculation: noop, })) const cleanupFns: (() => void)[] = [] mock.module('src/utils/cleanupRegistry.js', () => ({ + ...RealCleanupRegistry, registerCleanup: () => noop, })) mock.module('src/utils/abortController.js', () => ({ + ...RealAbortController, createAbortController: () => new AbortController(), createChildAbortController: (parent: AbortController) => { const ac = new AbortController() @@ -58,14 +80,17 @@ mock.module('src/utils/abortController.js', () => ({ })) mock.module('src/utils/task/sdkProgress.js', () => ({ + ...RealSdkProgress, emitTaskProgress: noop, })) mock.module('src/utils/sdkEventQueue.js', () => ({ + ...RealSdkEventQueue, enqueueSdkEvent: noop, })) mock.module('src/constants/xml.js', () => ({ + ...RealXml, TASK_NOTIFICATION_TAG: 'task_notification', TASK_ID_TAG: 'task_id', TOOL_USE_ID_TAG: 'tool_use_id', @@ -79,6 +104,7 @@ mock.module('src/constants/xml.js', () => ({ })) mock.module('src/services/analytics/index.js', () => ({ + ...RealAnalytics, logEvent: noop, logEventAsync: async () => {}, stripProtoFields: (v: any) => v, @@ -88,6 +114,7 @@ mock.module('src/services/analytics/index.js', () => ({ })) mock.module('src/utils/collapseReadSearch.js', () => ({ + ...RealCollapseReadSearch, getToolSearchOrReadInfo: () => undefined, }))