principles-disciple 1.72.0 → 1.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +10 -5
  3. package/package.json +17 -19
  4. package/scripts/acceptance-test.mjs +16 -73
  5. package/scripts/sync-plugin.mjs +382 -77
  6. package/src/commands/archive-impl.ts +2 -1
  7. package/src/commands/capabilities.ts +2 -2
  8. package/src/commands/context.ts +2 -2
  9. package/src/commands/disable-impl.ts +2 -1
  10. package/src/commands/evolution-status.ts +16 -16
  11. package/src/commands/export.ts +12 -67
  12. package/src/commands/pain.ts +91 -1
  13. package/src/commands/principle-rollback.ts +2 -1
  14. package/src/commands/promote-impl.ts +7 -43
  15. package/src/commands/rollback-impl.ts +2 -1
  16. package/src/commands/rollback.ts +2 -1
  17. package/src/commands/samples.ts +2 -1
  18. package/src/commands/thinking-os.ts +2 -1
  19. package/src/config/errors.ts +18 -2
  20. package/src/constants/diagnostician.ts +2 -2
  21. package/src/constants/tools.ts +2 -1
  22. package/src/core/__tests__/focus-history.test.ts +210 -0
  23. package/src/core/config.ts +1 -1
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +29 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/migration.ts +0 -1
  37. package/src/core/pain-diagnostic-gate.ts +154 -0
  38. package/src/core/pain-signal.ts +21 -138
  39. package/src/core/pain.ts +15 -88
  40. package/src/core/path-resolver.ts +0 -1
  41. package/src/core/paths.ts +0 -1
  42. package/src/core/pd-task-reconciler.ts +26 -115
  43. package/src/core/pd-task-service.ts +9 -9
  44. package/src/core/pd-task-types.ts +23 -127
  45. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  46. package/src/core/principle-compiler/code-validator.ts +15 -42
  47. package/src/core/principle-compiler/compiler.ts +100 -15
  48. package/src/core/principle-compiler/index.ts +5 -2
  49. package/src/core/principle-compiler/template-generator.ts +4 -104
  50. package/src/core/principle-injection.ts +10 -202
  51. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  52. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  53. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  54. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  55. package/src/core/principle-tree-ledger.ts +8 -6
  56. package/src/core/reflection/reflection-context.ts +14 -109
  57. package/src/core/replay-engine.ts +8 -500
  58. package/src/core/rule-host-helpers.ts +5 -35
  59. package/src/core/rule-host-types.ts +10 -82
  60. package/src/core/rule-host.ts +6 -63
  61. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  62. package/src/core/session-tracker.ts +87 -101
  63. package/src/core/shadow-observation-registry.ts +19 -48
  64. package/src/core/trajectory.ts +3 -1
  65. package/src/core/workflow-funnel-loader.ts +62 -68
  66. package/src/core/workspace-context.ts +46 -0
  67. package/src/core/workspace-dir-service.ts +1 -1
  68. package/src/core/workspace-dir-validation.ts +18 -9
  69. package/src/hooks/AGENTS.md +1 -1
  70. package/src/hooks/gate-block-helper.ts +71 -64
  71. package/src/hooks/gate.ts +183 -31
  72. package/src/hooks/lifecycle.ts +30 -32
  73. package/src/hooks/llm.ts +60 -32
  74. package/src/hooks/pain.ts +297 -103
  75. package/src/hooks/prompt.ts +400 -440
  76. package/src/hooks/subagent.ts +2 -29
  77. package/src/i18n/commands.ts +2 -10
  78. package/src/index.ts +95 -85
  79. package/src/openclaw-sdk.ts +311 -0
  80. package/src/service/central-database.ts +8 -4
  81. package/src/service/evolution-queue-migration.ts +2 -1
  82. package/src/service/evolution-worker.ts +163 -1786
  83. package/src/service/internalization-trigger-adapter.ts +302 -0
  84. package/src/service/keyword-optimization-service.ts +4 -4
  85. package/src/service/monitoring-query-service.ts +1 -215
  86. package/src/service/queue-io.ts +60 -331
  87. package/src/service/runtime-summary-service.ts +59 -16
  88. package/src/service/subagent-workflow/index.ts +0 -41
  89. package/src/service/subagent-workflow/types.ts +9 -120
  90. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  91. package/src/service/workflow-watchdog.ts +0 -43
  92. package/src/types/event-payload.ts +16 -74
  93. package/src/types/event-types.ts +38 -547
  94. package/src/types/hygiene-types.ts +7 -30
  95. package/src/types/principle-tree-schema.ts +20 -222
  96. package/src/types/queue.ts +15 -70
  97. package/src/types/runtime-summary.ts +5 -49
  98. package/src/utils/io.ts +8 -20
  99. package/src/utils/retry.ts +1 -1
  100. package/src/utils/shadow-fingerprint.ts +2 -2
  101. package/src/utils/workspace-resolver.ts +50 -0
  102. package/templates/langs/en/core/AGENTS.md +7 -7
  103. package/templates/langs/en/core/BOOT.md +1 -1
  104. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  105. package/templates/langs/en/principles/THINKING_OS.md +3 -2
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  107. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  108. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  109. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  110. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  111. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  112. package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
  113. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  114. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  115. package/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  116. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  117. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  118. package/templates/langs/zh/core/AGENTS.md +7 -7
  119. package/templates/langs/zh/core/BOOT.md +1 -1
  120. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  121. package/templates/langs/zh/principles/THINKING_OS.md +3 -2
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  127. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  128. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  129. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  130. package/templates/langs/zh/skills/evolve-task/SKILL.md +4 -4
  131. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  132. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  133. package/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  134. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  135. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  136. package/tests/build-artifacts.test.ts +1 -3
  137. package/tests/commands/evolution-status.test.ts +0 -118
  138. package/tests/core/bootstrap-rules.test.ts +1 -1
  139. package/tests/core/config.test.ts +1 -1
  140. package/tests/core/event-log.test.ts +35 -0
  141. package/tests/core/evolution-engine.test.ts +610 -0
  142. package/tests/core/file-store.test.ts +102 -0
  143. package/tests/core/focus-history.test.ts +203 -11
  144. package/tests/core/merge-gate-audit.test.ts +2 -169
  145. package/tests/core/migration.test.ts +7 -7
  146. package/tests/core/model-deployment-registry.test.ts +7 -1
  147. package/tests/core/model-training-registry.test.ts +19 -0
  148. package/tests/core/observability.test.ts +0 -1
  149. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  150. package/tests/core/pain.test.ts +0 -1
  151. package/tests/core/path-resolver.test.ts +1 -1
  152. package/tests/core/paths-refactor.test.ts +0 -22
  153. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  154. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  155. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  156. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  157. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  158. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  159. package/tests/core/reflection-context.test.ts +0 -14
  160. package/tests/core/replay-engine.test.ts +127 -215
  161. package/tests/core/rule-host-helpers.test.ts +2 -2
  162. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  163. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  164. package/tests/core/workspace-context.test.ts +2 -2
  165. package/tests/core/workspace-dir-validation.test.ts +8 -1
  166. package/tests/core-anti-growth.test.ts +191 -0
  167. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  168. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  169. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  170. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  171. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  172. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  173. package/tests/hooks/pain.test.ts +269 -12
  174. package/tests/hooks/prompt-characterization.test.ts +500 -0
  175. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  176. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  177. package/tests/index.test.ts +94 -1
  178. package/tests/integration/auto-entry-gate.test.ts +248 -0
  179. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  180. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  181. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  182. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  183. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  184. package/tests/service/central-database.test.ts +457 -0
  185. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  186. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  187. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  188. package/tests/service/monitoring-query-service.test.ts +1 -47
  189. package/tests/service/queue-io.test.ts +1 -62
  190. package/tests/service/runtime-summary-service.test.ts +3 -1
  191. package/tests/service/workflow-watchdog.test.ts +0 -91
  192. package/tests/utils/file-lock.test.ts +5 -3
  193. package/tests/utils/session-key.test.ts +52 -0
  194. package/tests/utils/subagent-probe.test.ts +48 -1
  195. package/vitest.config.ts +4 -11
  196. package/.planning/codebase/ARCHITECTURE.md +0 -157
  197. package/.planning/codebase/CONCERNS.md +0 -145
  198. package/.planning/codebase/CONVENTIONS.md +0 -148
  199. package/.planning/codebase/INTEGRATIONS.md +0 -81
  200. package/.planning/codebase/STACK.md +0 -87
  201. package/.planning/codebase/STRUCTURE.md +0 -193
  202. package/.planning/codebase/TESTING.md +0 -243
  203. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  204. package/docs/COMMAND_REFERENCE.md +0 -76
  205. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  206. package/scripts/build-web.mjs +0 -46
  207. package/scripts/diagnose-nocturnal.mjs +0 -537
  208. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  209. package/src/commands/nocturnal-review.ts +0 -322
  210. package/src/commands/nocturnal-rollout.ts +0 -790
  211. package/src/commands/nocturnal-train.ts +0 -986
  212. package/src/commands/pd-reflect.ts +0 -88
  213. package/src/core/adaptive-thresholds.ts +0 -478
  214. package/src/core/diagnostician-task-store.ts +0 -192
  215. package/src/core/nocturnal-arbiter.ts +0 -715
  216. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  217. package/src/core/nocturnal-artificer.ts +0 -257
  218. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  219. package/src/core/nocturnal-compliance.ts +0 -1146
  220. package/src/core/nocturnal-dataset.ts +0 -763
  221. package/src/core/nocturnal-executability.ts +0 -428
  222. package/src/core/nocturnal-export.ts +0 -499
  223. package/src/core/nocturnal-paths.ts +0 -240
  224. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  225. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  226. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  227. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  228. package/src/core/nocturnal-trinity-types.ts +0 -218
  229. package/src/core/nocturnal-trinity.ts +0 -2680
  230. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  231. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  232. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  233. package/src/http/principles-console-route.ts +0 -709
  234. package/src/service/central-health-service.ts +0 -49
  235. package/src/service/central-overview-service.ts +0 -138
  236. package/src/service/control-ui-query-service.ts +0 -900
  237. package/src/service/cooldown-strategy.ts +0 -97
  238. package/src/service/evolution-pain-context.ts +0 -79
  239. package/src/service/evolution-query-service.ts +0 -407
  240. package/src/service/health-query-service.ts +0 -1038
  241. package/src/service/nocturnal-config.ts +0 -214
  242. package/src/service/nocturnal-runtime.ts +0 -734
  243. package/src/service/nocturnal-service.ts +0 -1605
  244. package/src/service/nocturnal-target-selector.ts +0 -545
  245. package/src/service/sleep-cycle.ts +0 -157
  246. package/src/service/startup-reconciler.ts +0 -112
  247. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  248. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  249. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  250. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  251. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  252. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  253. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  254. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  255. package/src/tools/write-pain-flag.ts +0 -215
  256. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  257. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  258. package/tests/commands/nocturnal-review.test.ts +0 -448
  259. package/tests/commands/nocturnal-train.test.ts +0 -97
  260. package/tests/commands/pd-reflect.test.ts +0 -49
  261. package/tests/core/adaptive-thresholds.test.ts +0 -261
  262. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  263. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  264. package/tests/core/nocturnal-artificer.test.ts +0 -241
  265. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  266. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  267. package/tests/core/nocturnal-compliance.test.ts +0 -646
  268. package/tests/core/nocturnal-dataset.test.ts +0 -892
  269. package/tests/core/nocturnal-e2e.test.ts +0 -234
  270. package/tests/core/nocturnal-executability.test.ts +0 -357
  271. package/tests/core/nocturnal-export.test.ts +0 -517
  272. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  273. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  274. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  275. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  276. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  277. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  278. package/tests/core/pain-auto-repair.test.ts +0 -96
  279. package/tests/core/pain-integration.test.ts +0 -510
  280. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  281. package/tests/http/principles-console-route.test.ts +0 -162
  282. package/tests/integration/chaos-resilience.test.ts +0 -348
  283. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  284. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  285. package/tests/service/control-ui-query-service.test.ts +0 -121
  286. package/tests/service/cooldown-strategy.test.ts +0 -164
  287. package/tests/service/data-endpoints-regression.test.ts +0 -834
  288. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  289. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  290. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  291. package/tests/service/nocturnal-runtime.test.ts +0 -473
  292. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  293. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  294. package/tests/service/startup-reconciler.test.ts +0 -148
  295. package/tests/tools/write-pain-flag.test.ts +0 -358
  296. package/ui/src/App.tsx +0 -45
  297. package/ui/src/api.ts +0 -220
  298. package/ui/src/charts.tsx +0 -955
  299. package/ui/src/components/ErrorState.tsx +0 -6
  300. package/ui/src/components/Loading.tsx +0 -13
  301. package/ui/src/components/ProtectedRoute.tsx +0 -12
  302. package/ui/src/components/Shell.tsx +0 -91
  303. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  304. package/ui/src/components/index.ts +0 -5
  305. package/ui/src/context/auth.tsx +0 -80
  306. package/ui/src/context/theme.tsx +0 -66
  307. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  308. package/ui/src/i18n/ui.ts +0 -473
  309. package/ui/src/main.tsx +0 -16
  310. package/ui/src/pages/EvolutionPage.tsx +0 -333
  311. package/ui/src/pages/FeedbackPage.tsx +0 -138
  312. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  313. package/ui/src/pages/LoginPage.tsx +0 -89
  314. package/ui/src/pages/OverviewPage.tsx +0 -599
  315. package/ui/src/pages/SamplesPage.tsx +0 -174
  316. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  317. package/ui/src/styles.css +0 -2020
  318. package/ui/src/types.ts +0 -384
  319. package/ui/src/utils/format.ts +0 -15
@@ -0,0 +1,457 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
2
+ import { rmSync, mkdirSync, readdirSync, existsSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import os from 'os';
5
+ import Database from 'better-sqlite3';
6
+
7
+ import { CentralDatabase, getCentralDatabase, resetCentralDatabase } from '../../src/service/central-database.js';
8
+
9
+ // ── Home dir ────────────────────────────────────────────────────────────────
10
+
11
+ const REAL_HOME = os.homedir();
12
+ const OPENCLAW_DIR = join(REAL_HOME, '.openclaw');
13
+ const CENTRAL_DB_DIR = '.central';
14
+
15
+ /** Each test gets its own DB file via PD_CENTRAL_DB_PATH env var. */
16
+ let tempDbDir = '';
17
+
18
+ function makeTempDbPath(): string {
19
+ const dir = join(os.tmpdir(), `pd-cdb-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
20
+ mkdirSync(dir, { recursive: true });
21
+ return join(dir, 'aggregated.db');
22
+ }
23
+
24
+ function cleanupTestDirs() {
25
+ // Clean up workspace dirs (cdb-test-* names) and .central/
26
+ if (existsSync(OPENCLAW_DIR)) {
27
+ for (const entry of readdirSync(OPENCLAW_DIR)) {
28
+ if (entry.startsWith('cdb-test-') || entry === '.central') {
29
+ try {
30
+ rmSync(join(OPENCLAW_DIR, entry), { recursive: true, force: true });
31
+ } catch { /* noop */ }
32
+ }
33
+ }
34
+ }
35
+ // Clean up temp DB dirs from this run
36
+ if (tempDbDir) {
37
+ try { rmSync(tempDbDir, { recursive: true, force: true }); } catch { /* noop */ }
38
+ }
39
+ }
40
+
41
+ beforeEach(() => {
42
+ cleanupTestDirs();
43
+ const dbPath = makeTempDbPath();
44
+ tempDbDir = dirname(dbPath);
45
+ process.env.PD_CENTRAL_DB_PATH = dbPath;
46
+ resetCentralDatabase();
47
+ });
48
+
49
+ afterEach(() => {
50
+ resetCentralDatabase();
51
+ delete process.env.PD_CENTRAL_DB_PATH;
52
+ cleanupTestDirs();
53
+ });
54
+
55
+ function makeWorkspaceName(prefix: string) {
56
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
57
+ }
58
+
59
+ // ── Trajectory DB helper ────────────────────────────────────────────────────
60
+
61
+ function makeTrajectoryDb(workspaceName: string) {
62
+ // openclawDir = ~/.openclaw, so workspace dir is ~/.openclaw/<workspaceName>
63
+ const wsDir = join(OPENCLAW_DIR, workspaceName);
64
+ mkdirSync(wsDir, { recursive: true });
65
+ const stateDir = join(wsDir, '.state');
66
+ mkdirSync(stateDir, { recursive: true });
67
+ const db = new Database(join(stateDir, 'trajectory.db'));
68
+ db.exec(`
69
+ CREATE TABLE IF NOT EXISTS sessions (session_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, updated_at TEXT NOT NULL);
70
+ CREATE TABLE IF NOT EXISTS tool_calls (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, tool_name TEXT NOT NULL, outcome TEXT NOT NULL, duration_ms INTEGER, error_type TEXT, error_message TEXT, created_at TEXT NOT NULL);
71
+ CREATE TABLE IF NOT EXISTS pain_events (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, source TEXT NOT NULL, score REAL NOT NULL, reason TEXT, created_at TEXT NOT NULL);
72
+ CREATE TABLE IF NOT EXISTS user_turns (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, correction_cue TEXT, correction_detected INTEGER DEFAULT 0, created_at TEXT NOT NULL);
73
+ CREATE TABLE IF NOT EXISTS principle_events (id INTEGER PRIMARY KEY AUTOINCREMENT, principle_id TEXT, event_type TEXT NOT NULL, created_at TEXT NOT NULL);
74
+ CREATE TABLE IF NOT EXISTS thinking_model_events (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, model_id TEXT NOT NULL, matched_pattern TEXT NOT NULL, created_at TEXT NOT NULL);
75
+ CREATE TABLE IF NOT EXISTS correction_samples (sample_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, bad_assistant_turn_id INTEGER NOT NULL, quality_score REAL, review_status TEXT, created_at TEXT NOT NULL);
76
+ CREATE TABLE IF NOT EXISTS task_outcomes (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, task_id TEXT, outcome TEXT NOT NULL, created_at TEXT NOT NULL);
77
+ `);
78
+ return { db, wsDir };
79
+ }
80
+
81
+ function seedSessions(db: Database.Database, count: number) {
82
+ for (let i = 0; i < count; i++) {
83
+ db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?)')
84
+ .run(`sess-${i}`, '2026-04-01T00:00:00Z', '2026-04-01T00:10:00Z');
85
+ }
86
+ }
87
+
88
+ function seedToolCalls(db: Database.Database, successes = 0, failures = 0, errorType = 'ENOENT') {
89
+ for (let i = 0; i < successes; i++) {
90
+ db.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, created_at) VALUES (?, ?, ?, ?, ?)')
91
+ .run('sess-0', 'write_file', 'success', 100, '2026-04-01T00:00:00Z');
92
+ }
93
+ for (let i = 0; i < failures; i++) {
94
+ db.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, error_type, created_at) VALUES (?, ?, ?, ?, ?, ?)')
95
+ .run('sess-0', 'bash', 'failure', 100, errorType, '2026-04-01T00:00:00Z');
96
+ }
97
+ }
98
+
99
+ function seedPainEvents(db: Database.Database, count: number) {
100
+ for (let i = 0; i < count; i++) {
101
+ db.prepare('INSERT INTO pain_events (session_id, source, score, reason, created_at) VALUES (?, ?, ?, ?, ?)')
102
+ .run('sess-0', 'tool_failure', 75, 'test', '2026-04-01T00:00:00Z');
103
+ }
104
+ }
105
+
106
+ function seedCorrections(db: Database.Database, count: number) {
107
+ for (let i = 0; i < count; i++) {
108
+ db.prepare('INSERT INTO user_turns (session_id, correction_cue, correction_detected, created_at) VALUES (?, ?, 1, ?)')
109
+ .run('sess-0', 'stop doing that', '2026-04-01T00:00:00Z');
110
+ }
111
+ }
112
+
113
+ function seedPrincipleEvents(db: Database.Database, count: number) {
114
+ for (let i = 0; i < count; i++) {
115
+ db.prepare('INSERT INTO principle_events (principle_id, event_type, created_at) VALUES (?, ?, ?)')
116
+ .run(`p-${i}`, 'candidate_created', '2026-04-01T00:00:00Z');
117
+ }
118
+ }
119
+
120
+ function seedThinkingEvents(db: Database.Database, count: number) {
121
+ for (let i = 0; i < count; i++) {
122
+ db.prepare('INSERT INTO thinking_model_events (session_id, model_id, matched_pattern, created_at) VALUES (?, ?, ?, ?)')
123
+ .run('sess-0', 'claude-3-5-sonnet', 'error_pattern', '2026-04-01T00:00:00Z');
124
+ }
125
+ }
126
+
127
+ function seedSamples(db: Database.Database) {
128
+ db.prepare('INSERT INTO correction_samples (sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at) VALUES (?, ?, ?, ?, ?, ?)')
129
+ .run('s1', 'sess-0', 1, 0.9, 'pending', '2026-04-01T00:00:00Z');
130
+ db.prepare('INSERT INTO correction_samples (sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at) VALUES (?, ?, ?, ?, ?, ?)')
131
+ .run('s2', 'sess-0', 1, 0.7, 'approved', '2026-04-01T00:00:00Z');
132
+ db.prepare('INSERT INTO correction_samples (sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at) VALUES (?, ?, ?, ?, ?, ?)')
133
+ .run('s3', 'sess-0', 1, 0.5, 'rejected', '2026-04-01T00:00:00Z');
134
+ }
135
+
136
+ function seedTaskOutcomes(db: Database.Database, count: number) {
137
+ for (let i = 0; i < count; i++) {
138
+ db.prepare('INSERT INTO task_outcomes (session_id, task_id, outcome, created_at) VALUES (?, ?, ?, ?)')
139
+ .run('sess-0', `task-${i}`, 'success', '2026-04-01T00:00:00Z');
140
+ }
141
+ }
142
+
143
+ // ── Tests ──────────────────────────────────────────────────────────────────
144
+
145
+ describe('CentralDatabase', () => {
146
+ test('creates schema on construction', () => {
147
+ const db = new CentralDatabase();
148
+ expect(() => db.getOverviewStats()).not.toThrow();
149
+ db.dispose();
150
+ });
151
+
152
+ test('isClosed reflects disposal state', () => {
153
+ const db = new CentralDatabase();
154
+ expect(db.isClosed).toBe(false);
155
+ db.dispose();
156
+ expect(db.isClosed).toBe(true);
157
+ });
158
+
159
+ test('dispose prevents further operations', () => {
160
+ const db = new CentralDatabase();
161
+ db.dispose();
162
+ expect(() => db.getOverviewStats()).toThrow();
163
+ });
164
+
165
+ test('getOverviewStats returns zeros when no data', () => {
166
+ const db = new CentralDatabase();
167
+ const stats = db.getOverviewStats();
168
+ expect(stats.totalSessions).toBe(0);
169
+ expect(stats.totalToolCalls).toBe(0);
170
+ expect(stats.totalFailures).toBe(0);
171
+ expect(stats.totalPainEvents).toBe(0);
172
+ db.dispose();
173
+ });
174
+
175
+ test('getTopRegressions returns empty array when no failures', () => {
176
+ const db = new CentralDatabase();
177
+ expect(db.getTopRegressions()).toEqual([]);
178
+ db.dispose();
179
+ });
180
+
181
+ test('addCustomWorkspace inserts and enables workspace', () => {
182
+ const db = new CentralDatabase();
183
+ db.addCustomWorkspace('my-ws', '/custom/path');
184
+ const configs = db.getWorkspaceConfigs();
185
+ const found = configs.find(c => c.workspaceName === 'my-ws');
186
+ expect(found).toBeDefined();
187
+ expect(found?.enabled).toBe(true);
188
+ expect(found?.syncEnabled).toBe(true);
189
+ db.dispose();
190
+ });
191
+
192
+ test('updateWorkspaceConfig updates displayName and enabled', () => {
193
+ const db = new CentralDatabase();
194
+ db.addCustomWorkspace('cfg-ws', '/path');
195
+ db.updateWorkspaceConfig('cfg-ws', { displayName: 'Display', enabled: false });
196
+ const configs = db.getWorkspaceConfigs();
197
+ const found = configs.find(c => c.workspaceName === 'cfg-ws');
198
+ expect(found?.displayName).toBe('Display');
199
+ expect(found?.enabled).toBe(false);
200
+ db.dispose();
201
+ });
202
+
203
+ test('isWorkspaceEnabled returns true when no config', () => {
204
+ const db = new CentralDatabase();
205
+ expect(db.isWorkspaceEnabled('unknown')).toBe(true);
206
+ db.dispose();
207
+ });
208
+
209
+ test('isWorkspaceEnabled returns false when disabled', () => {
210
+ const db = new CentralDatabase();
211
+ db.addCustomWorkspace('dis-ws', '/path');
212
+ db.updateWorkspaceConfig('dis-ws', { enabled: false });
213
+ expect(db.isWorkspaceEnabled('dis-ws')).toBe(false);
214
+ db.dispose();
215
+ });
216
+
217
+ test('removeWorkspace disables workspace', () => {
218
+ const db = new CentralDatabase();
219
+ db.addCustomWorkspace('rem-ws', '/path');
220
+ db.removeWorkspace('rem-ws');
221
+ expect(db.isWorkspaceEnabled('rem-ws')).toBe(false);
222
+ db.dispose();
223
+ });
224
+
225
+ test('setGlobalConfig and getGlobalConfig round-trip', () => {
226
+ const db = new CentralDatabase();
227
+ db.setGlobalConfig('key', 'value');
228
+ expect(db.getGlobalConfig('key')).toBe('value');
229
+ db.setGlobalConfig('key', 'value2');
230
+ expect(db.getGlobalConfig('key')).toBe('value2');
231
+ expect(db.getGlobalConfig('missing')).toBeNull();
232
+ db.dispose();
233
+ });
234
+
235
+ test('clearAll deletes all aggregated data', () => {
236
+ const db = new CentralDatabase();
237
+ db.clearAll();
238
+ expect(db.getOverviewStats().totalSessions).toBe(0);
239
+ db.dispose();
240
+ });
241
+
242
+ // ── syncWorkspace ─────────────────────────────────────────────────────────
243
+
244
+ test('syncWorkspace throws for unknown workspace', () => {
245
+ const db = new CentralDatabase();
246
+ expect(() => db.syncWorkspace('nonexistent')).toThrow();
247
+ db.dispose();
248
+ });
249
+
250
+ test('syncWorkspace returns 0 when no trajectory.db', () => {
251
+ const wsName = makeWorkspaceName('cdb-test-empty');
252
+ const db = new CentralDatabase();
253
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
254
+ // Create workspace dir without trajectory.db
255
+ mkdirSync(join(OPENCLAW_DIR, wsName, '.state'), { recursive: true });
256
+ expect(db.syncWorkspace(wsName)).toBe(0);
257
+ db.dispose();
258
+ });
259
+
260
+ test('syncWorkspace syncs sessions', () => {
261
+ const wsName = makeWorkspaceName('cdb-test-sessions');
262
+ const { db: trajDb } = makeTrajectoryDb(wsName);
263
+ seedSessions(trajDb, 5);
264
+ trajDb.close();
265
+ const db = new CentralDatabase();
266
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
267
+ db.syncWorkspace(wsName);
268
+ expect(db.getOverviewStats().totalSessions).toBeGreaterThanOrEqual(5);
269
+ db.dispose();
270
+ });
271
+
272
+ test('syncWorkspace syncs tool calls with failures', () => {
273
+ const wsName = makeWorkspaceName('cdb-test-tools');
274
+ const { db: trajDb } = makeTrajectoryDb(wsName);
275
+ seedSessions(trajDb, 1);
276
+ seedToolCalls(trajDb, 3, 2, 'ENOENT');
277
+ trajDb.close();
278
+ const db = new CentralDatabase();
279
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
280
+ db.syncWorkspace(wsName);
281
+ const stats = db.getOverviewStats();
282
+ expect(stats.totalToolCalls).toBeGreaterThanOrEqual(5);
283
+ expect(stats.totalFailures).toBeGreaterThanOrEqual(2);
284
+ db.dispose();
285
+ });
286
+
287
+ test('syncWorkspace syncs pain events', () => {
288
+ const wsName = makeWorkspaceName('cdb-test-pain');
289
+ const { db: trajDb } = makeTrajectoryDb(wsName);
290
+ seedSessions(trajDb, 1);
291
+ seedPainEvents(trajDb, 4);
292
+ trajDb.close();
293
+ const db = new CentralDatabase();
294
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
295
+ db.syncWorkspace(wsName);
296
+ expect(db.getOverviewStats().totalPainEvents).toBeGreaterThanOrEqual(4);
297
+ db.dispose();
298
+ });
299
+
300
+ test('syncWorkspace syncs user corrections', () => {
301
+ const wsName = makeWorkspaceName('cdb-test-corr');
302
+ const { db: trajDb } = makeTrajectoryDb(wsName);
303
+ seedSessions(trajDb, 1);
304
+ seedCorrections(trajDb, 3);
305
+ trajDb.close();
306
+ const db = new CentralDatabase();
307
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
308
+ db.syncWorkspace(wsName);
309
+ expect(db.getOverviewStats().totalCorrections).toBeGreaterThanOrEqual(3);
310
+ db.dispose();
311
+ });
312
+
313
+ test('syncWorkspace syncs principle events', () => {
314
+ const wsName = makeWorkspaceName('cdb-test-principle');
315
+ const { db: trajDb } = makeTrajectoryDb(wsName);
316
+ seedSessions(trajDb, 1);
317
+ seedPrincipleEvents(trajDb, 2);
318
+ trajDb.close();
319
+ const db = new CentralDatabase();
320
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
321
+ db.syncWorkspace(wsName);
322
+ expect(db.getPrincipleEventCount()).toBeGreaterThanOrEqual(2);
323
+ db.dispose();
324
+ });
325
+
326
+ test('syncWorkspace syncs thinking model events', () => {
327
+ const wsName = makeWorkspaceName('cdb-test-thinking');
328
+ const { db: trajDb } = makeTrajectoryDb(wsName);
329
+ seedSessions(trajDb, 1);
330
+ seedThinkingEvents(trajDb, 3);
331
+ trajDb.close();
332
+ const db = new CentralDatabase();
333
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
334
+ db.syncWorkspace(wsName);
335
+ expect(db.getThinkingModelStats().totalModels).toBeGreaterThanOrEqual(1);
336
+ db.dispose();
337
+ });
338
+
339
+ test('syncWorkspace syncs correction samples with review_status', () => {
340
+ const wsName = makeWorkspaceName('cdb-test-samples');
341
+ const { db: trajDb } = makeTrajectoryDb(wsName);
342
+ seedSessions(trajDb, 1);
343
+ seedSamples(trajDb);
344
+ trajDb.close();
345
+ const db = new CentralDatabase();
346
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
347
+ db.syncWorkspace(wsName);
348
+ const counters = db.getSampleCountersByStatus();
349
+ expect(counters['pending']).toBeGreaterThanOrEqual(1);
350
+ expect(counters['approved']).toBeGreaterThanOrEqual(1);
351
+ expect(counters['rejected']).toBeGreaterThanOrEqual(1);
352
+ db.dispose();
353
+ });
354
+
355
+ test('syncWorkspace syncs task outcomes', () => {
356
+ const wsName = makeWorkspaceName('cdb-test-tasks');
357
+ const { db: trajDb } = makeTrajectoryDb(wsName);
358
+ seedSessions(trajDb, 1);
359
+ seedTaskOutcomes(trajDb, 4);
360
+ trajDb.close();
361
+ const db = new CentralDatabase();
362
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
363
+ db.syncWorkspace(wsName);
364
+ expect(db.getTaskOutcomes()).toBeGreaterThanOrEqual(4);
365
+ db.dispose();
366
+ });
367
+
368
+ test('syncWorkspace updates last_sync timestamp', () => {
369
+ const wsName = makeWorkspaceName('cdb-test-sync');
370
+ const { db: trajDb } = makeTrajectoryDb(wsName);
371
+ seedSessions(trajDb, 1);
372
+ trajDb.close();
373
+ const db = new CentralDatabase();
374
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
375
+ db.syncWorkspace(wsName);
376
+ const ws = db.getWorkspaces().find(w => w.name === wsName);
377
+ expect(ws?.lastSync).not.toBeNull();
378
+ db.dispose();
379
+ });
380
+
381
+ test('getTopRegressions returns error types ordered by occurrences', () => {
382
+ const wsName = makeWorkspaceName('cdb-test-regr');
383
+ const { db: trajDb } = makeTrajectoryDb(wsName);
384
+ seedSessions(trajDb, 1);
385
+ trajDb.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, error_type, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?)')
386
+ .run('sess-0', 'bash', 'failure', 'ENOENT', 100, '2026-04-01T00:00:00Z');
387
+ trajDb.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, error_type, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?)')
388
+ .run('sess-0', 'bash', 'failure', 'ENOENT', 100, '2026-04-01T00:01:00Z');
389
+ trajDb.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, error_type, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?)')
390
+ .run('sess-0', 'bash', 'failure', 'EPERM', 100, '2026-04-01T00:02:00Z');
391
+ trajDb.close();
392
+ const db = new CentralDatabase();
393
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
394
+ db.syncWorkspace(wsName);
395
+ const regressions = db.getTopRegressions(5);
396
+ expect(regressions[0].errorType).toBe('ENOENT');
397
+ expect(regressions[0].occurrences).toBe(2);
398
+ db.dispose();
399
+ });
400
+
401
+ test('getSamplePreview returns recent pending/approved samples', () => {
402
+ const wsName = makeWorkspaceName('cdb-test-preview');
403
+ const { db: trajDb } = makeTrajectoryDb(wsName);
404
+ seedSessions(trajDb, 1);
405
+ seedSamples(trajDb);
406
+ trajDb.close();
407
+ const db = new CentralDatabase();
408
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
409
+ db.syncWorkspace(wsName);
410
+ const preview = db.getSamplePreview(5);
411
+ expect(preview.length).toBeGreaterThanOrEqual(2);
412
+ db.dispose();
413
+ });
414
+
415
+ test('getMostRecentSync returns null when no sync', () => {
416
+ const db = new CentralDatabase();
417
+ expect(db.getMostRecentSync()).toBeNull();
418
+ db.dispose();
419
+ });
420
+
421
+ test('getMostRecentSync returns timestamp after sync', () => {
422
+ const wsName = makeWorkspaceName('cdb-test-recent');
423
+ const { db: trajDb } = makeTrajectoryDb(wsName);
424
+ seedSessions(trajDb, 1);
425
+ trajDb.close();
426
+ const db = new CentralDatabase();
427
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
428
+ db.syncWorkspace(wsName);
429
+ expect(db.getMostRecentSync()).not.toBeNull();
430
+ db.dispose();
431
+ });
432
+
433
+ // ── Singleton ────────────────────────────────────────────────────────────
434
+
435
+ test('getCentralDatabase returns same instance', () => {
436
+ const db1 = getCentralDatabase();
437
+ const db2 = getCentralDatabase();
438
+ expect(db1).toBe(db2);
439
+ resetCentralDatabase();
440
+ });
441
+
442
+ test('resetCentralDatabase clears instance', () => {
443
+ const db1 = getCentralDatabase();
444
+ resetCentralDatabase();
445
+ const db2 = getCentralDatabase();
446
+ expect(db1).not.toBe(db2);
447
+ resetCentralDatabase();
448
+ });
449
+
450
+ test('getCentralDatabase reopens after close', () => {
451
+ const db1 = getCentralDatabase();
452
+ db1.dispose();
453
+ const db2 = getCentralDatabase();
454
+ expect(db2.isClosed).toBe(false);
455
+ resetCentralDatabase();
456
+ });
457
+ });
@@ -0,0 +1,173 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import { WorkspaceContext } from '../../src/core/workspace-context.js';
6
+ import { ConfigService } from '../../src/core/config-service.js';
7
+
8
+ // Shared mocks
9
+ const mockLearner = {
10
+ getStore: vi.fn(() => ({ keywords: [{ term: 'wrong', weight: 0.5 }] })),
11
+ };
12
+
13
+ const mockDb = {
14
+ listRecentSessions: vi.fn(() => [{ sessionId: 'session-1' }]),
15
+ listUserTurnsForSession: vi.fn(() => [{ rawExcerpt: 'User said wrong input', correctionDetected: true, correctionCue: 'wrong' }]),
16
+ };
17
+
18
+ const mockOptimizationService = {
19
+ buildTrajectoryHistory: vi.fn(async () => [
20
+ { sessionId: 'session-1', timestamp: 'now', term: 'wrong', userMessage: '' }
21
+ ]),
22
+ applyResult: vi.fn(),
23
+ };
24
+
25
+ // Mock dependencies
26
+ vi.mock('../../src/core/correction-cue-learner.js', () => ({
27
+ CorrectionCueLearner: { get: vi.fn(() => mockLearner) },
28
+ }));
29
+
30
+ vi.mock('../../src/core/trajectory.js', () => ({
31
+ TrajectoryRegistry: {
32
+ get: vi.fn(() => mockDb),
33
+ clear: vi.fn(),
34
+ },
35
+ }));
36
+
37
+ vi.mock('../../src/service/keyword-optimization-service.js', () => ({
38
+ KeywordOptimizationService: { get: vi.fn(() => mockOptimizationService) },
39
+ }));
40
+
41
+ // Mock principles-core runtime-v2 observer/scheduler classes
42
+ const mockDispatch = vi.fn().mockResolvedValue({
43
+ updated: true,
44
+ summary: 'Keyword store optimized',
45
+ updates: { wrong: { action: 'update', weight: 0.4, reasoning: 'slightly high FP' } }
46
+ });
47
+
48
+ const mockRegister = vi.fn();
49
+
50
+ vi.mock('@principles/core/runtime-v2', () => {
51
+ return {
52
+ WorkflowFunnelLoader: class {
53
+ getFunnel = vi.fn(() => ({
54
+ policy: {
55
+ runtimeKind: 'pi-ai',
56
+ provider: 'anthropic',
57
+ model: 'anthropic/claude-3-5-sonnet',
58
+ apiKeyEnv: 'ANTHROPIC_API_KEY',
59
+ timeoutMs: 30000,
60
+ }
61
+ }));
62
+ },
63
+ PiAiRuntimeAdapter: class {},
64
+ CorrectionObserver: class {},
65
+ AgentScheduler: class {
66
+ register = mockRegister;
67
+ dispatch = mockDispatch;
68
+ }
69
+ };
70
+ });
71
+
72
+ // Import EvolutionWorkerService
73
+ import { EvolutionWorkerService } from '../../src/service/evolution-worker.js';
74
+ import { safeRmDir } from '../test-utils.js';
75
+
76
+ function createMockApi() {
77
+ return {
78
+ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
79
+ runtime: {
80
+ agent: { runEmbeddedPiAgent: vi.fn() },
81
+ system: {
82
+ requestHeartbeatNow: vi.fn(),
83
+ runHeartbeatOnce: vi.fn(),
84
+ },
85
+ },
86
+ } as any;
87
+ }
88
+
89
+ const fastPollConfig = { get: (k: string) => k === 'intervals.worker_poll_ms' ? 1000 : undefined };
90
+
91
+ describe('EvolutionWorkerService Correction Observer Integration', () => {
92
+ beforeEach(() => {
93
+ vi.useFakeTimers();
94
+ vi.clearAllMocks();
95
+ EvolutionWorkerService.api = null;
96
+ });
97
+
98
+ afterEach(() => {
99
+ vi.useRealTimers();
100
+ EvolutionWorkerService.api = null;
101
+ });
102
+
103
+ it('runs Correction Observer on heartbeat and applies updates when configured', async () => {
104
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-worker-corr-'));
105
+ const stateDir = path.join(workspaceDir, '.state');
106
+ fs.mkdirSync(stateDir, { recursive: true });
107
+
108
+ // Initialize an empty queue to avoid processing actual queue items
109
+ fs.writeFileSync(
110
+ path.join(stateDir, 'evolution_queue.json'),
111
+ JSON.stringify([], null, 2),
112
+ 'utf8'
113
+ );
114
+
115
+ // Initialize pain settings with fast poll interval
116
+ fs.writeFileSync(
117
+ path.join(stateDir, 'pain_settings.json'),
118
+ JSON.stringify({
119
+ intervals: {
120
+ worker_poll_ms: 1000
121
+ }
122
+ }, null, 2),
123
+ 'utf8'
124
+ );
125
+
126
+ // Invalidate workspace cache to load the newly written settings
127
+ WorkspaceContext.clearCache();
128
+ ConfigService.reset();
129
+
130
+ const mockApi = createMockApi();
131
+ EvolutionWorkerService.api = mockApi;
132
+
133
+ try {
134
+ EvolutionWorkerService.start({
135
+ workspaceDir,
136
+ stateDir,
137
+ logger: mockApi.logger,
138
+ config: fastPollConfig,
139
+ api: mockApi,
140
+ } as any);
141
+
142
+ // 1. Advance to startup timer (5000ms) and wait for microtasks to settle
143
+ await vi.advanceTimersByTimeAsync(5000);
144
+ for (let i = 0; i < 20; i++) {
145
+ await Promise.resolve();
146
+ }
147
+
148
+ // 2. Advance past the poll interval (1000ms) to trigger runCycle
149
+ await vi.advanceTimersByTimeAsync(1050);
150
+ for (let i = 0; i < 20; i++) {
151
+ await Promise.resolve();
152
+ }
153
+
154
+ // Verify that the scheduler dispatch was called with correct payload
155
+ expect(mockRegister).toHaveBeenCalled();
156
+ expect(mockDispatch).toHaveBeenCalledWith('correction-observer', expect.objectContaining({
157
+ parentSessionId: 'evolution-worker',
158
+ workspaceDir,
159
+ recentMessages: ['User said wrong input'],
160
+ }));
161
+
162
+ // Verify updates were applied using KeywordOptimizationService
163
+ expect(mockOptimizationService.applyResult).toHaveBeenCalledWith(expect.objectContaining({
164
+ updated: true,
165
+ summary: 'Keyword store optimized',
166
+ }));
167
+
168
+ } finally {
169
+ EvolutionWorkerService.stop!({ workspaceDir, stateDir, logger: console } as any);
170
+ safeRmDir(workspaceDir);
171
+ }
172
+ });
173
+ });