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
@@ -1,380 +0,0 @@
1
- /**
2
- * Pain → Diagnostician Loop E2E Tests
3
- *
4
- * PURPOSE: Verify business invariants in the Pain → Diagnostician loop.
5
- * These tests are designed to DISCOVER bugs, not just confirm existing behavior.
6
- *
7
- * DESIGN PRINCIPLES:
8
- * 1. Use real file system (no mocks for I/O)
9
- * 2. Test business invariants, not implementation details
10
- * 3. Use independent Oracle data sources for verification
11
- * 4. Test resilience (corruption recovery, concurrency safety)
12
- *
13
- * DATA FLOW:
14
- * after_tool_call (hooks/pain.ts)
15
- * → writePainFlag → .state/.pain_flag
16
- * Evolution Worker (service/evolution-worker.ts)
17
- * → enqueues pain_diagnosis task → evolution_queue.json
18
- * → addDiagnosticianTask → diagnostician_tasks.json
19
- * before_prompt_build (hooks/prompt.ts)
20
- * → getPendingDiagnosticianTasks → inject into prompt
21
- */
22
-
23
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
24
- import * as fs from 'fs';
25
- import * as os from 'os';
26
- import * as path from 'path';
27
- import {
28
- buildPainFlag,
29
- writePainFlag,
30
- readPainFlagData,
31
- validatePainFlag,
32
- } from '../../src/core/pain.js';
33
- import { getPendingDiagnosticianTasks, addDiagnosticianTask } from '../../src/core/diagnostician-task-store.js';
34
-
35
- // ─────────────────────────────────────────────────────────────────────
36
- // Helper functions
37
- // ─────────────────────────────────────────────────────────────────────
38
-
39
- function createTestWorkspace(): { workspaceDir: string; stateDir: string } {
40
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-e2e-pain-'));
41
- const stateDir = path.join(workspaceDir, '.state');
42
- fs.mkdirSync(stateDir, { recursive: true });
43
- fs.mkdirSync(path.join(workspaceDir, '.principles'), { recursive: true });
44
- return { workspaceDir, stateDir };
45
- }
46
-
47
- function cleanupWorkspace(workspaceDir: string): void {
48
- try {
49
- fs.rmSync(workspaceDir, { recursive: true, force: true });
50
- } catch {
51
- // ignore
52
- }
53
- }
54
-
55
- // ─────────────────────────────────────────────────────────────────────
56
- // PART 1: Business Invariants
57
- // Tests that verify system MUST maintain these rules
58
- // ─────────────────────────────────────────────────────────────────────
59
-
60
- describe('Pain → Diagnostician: Business Invariants', () => {
61
- let workspaceDir: string;
62
- let stateDir: string;
63
-
64
- beforeEach(() => {
65
- const ws = createTestWorkspace();
66
- workspaceDir = ws.workspaceDir;
67
- stateDir = ws.stateDir;
68
- });
69
-
70
- afterEach(() => {
71
- cleanupWorkspace(workspaceDir);
72
- });
73
-
74
- // ── INVARIANT 1: Pain flag format contract ──
75
- describe('INVARIANT: Pain flag format contract', () => {
76
- it('MUST contain all required fields after writePainFlag', () => {
77
- const data = buildPainFlag({
78
- source: 'tool_failure',
79
- score: '70',
80
- reason: 'Command failed with exit code 1',
81
- session_id: 'test-session-123',
82
- agent_id: 'main',
83
- is_risky: true,
84
- });
85
-
86
- writePainFlag(workspaceDir, data);
87
-
88
- // Independent verification: read file directly, don't trust writePainFlag
89
- const painFlagPath = path.join(stateDir, '.pain_flag');
90
- expect(fs.existsSync(painFlagPath)).toBe(true);
91
-
92
- const content = fs.readFileSync(painFlagPath, 'utf-8');
93
-
94
- // INVARIANT: All required fields MUST be present
95
- expect(content).toContain('source: tool_failure');
96
- expect(content).toContain('score: 70');
97
- expect(content).toMatch(/time: \d{4}-\d{2}-\d{2}T/); // ISO timestamp with space
98
- expect(content).toContain('reason:');
99
- expect(content).toContain('is_risky: true');
100
- });
101
-
102
- it('MUST NOT write empty optional fields to disk', () => {
103
- const data = buildPainFlag({
104
- source: 'human_intervention',
105
- score: '50',
106
- reason: 'User feedback',
107
- // Optional fields omitted
108
- });
109
-
110
- writePainFlag(workspaceDir, data);
111
-
112
- const content = fs.readFileSync(path.join(stateDir, '.pain_flag'), 'utf-8');
113
-
114
- // INVARIANT: Empty optional fields MUST NOT appear in file
115
- // This prevents confusion when reading the file
116
- expect(content).not.toMatch(/trace_id:\s*$/m);
117
- expect(content).not.toMatch(/trigger_text_preview:\s*$/m);
118
- });
119
-
120
- it('score MUST be in valid range 0-100', () => {
121
- // Test boundary values
122
- const scores = ['0', '50', '100'];
123
-
124
- for (const score of scores) {
125
- const data = buildPainFlag({
126
- source: 'test',
127
- score,
128
- reason: 'Test',
129
- });
130
-
131
- writePainFlag(workspaceDir, data);
132
-
133
- const read = readPainFlagData(workspaceDir);
134
- const numScore = Number(read.score);
135
-
136
- // INVARIANT: Score MUST be in valid range
137
- expect(numScore).toBeGreaterThanOrEqual(0);
138
- expect(numScore).toBeLessThanOrEqual(100);
139
- }
140
- });
141
- });
142
-
143
- // ── INVARIANT 2: Pain flag validation contract ──
144
- describe('INVARIANT: Pain flag validation', () => {
145
- it('validatePainFlag MUST reject flags missing required fields', () => {
146
- const invalidFlags = [
147
- { source: '', score: '50', time: '2024-01-01', reason: 'test' }, // empty source
148
- { source: 'test', score: '', time: '2024-01-01', reason: 'test' }, // empty score
149
- { source: 'test', score: '50', time: '', reason: 'test' }, // empty time
150
- { source: 'test', score: '50', time: '2024-01-01', reason: '' }, // empty reason
151
- ];
152
-
153
- for (const flag of invalidFlags) {
154
- const missing = validatePainFlag(flag);
155
- // INVARIANT: Missing required fields MUST be detected
156
- expect(missing.length).toBeGreaterThan(0);
157
- }
158
- });
159
-
160
- it('validatePainFlag MUST accept valid flags', () => {
161
- const validFlag = {
162
- source: 'tool_failure',
163
- score: '70',
164
- time: new Date().toISOString(),
165
- reason: 'Command failed',
166
- session_id: 'test',
167
- agent_id: 'main',
168
- is_risky: 'false',
169
- };
170
-
171
- const missing = validatePainFlag(validFlag);
172
- // INVARIANT: Valid flags MUST pass validation
173
- expect(missing).toEqual([]);
174
- });
175
- });
176
-
177
- // ── INVARIANT 3: Diagnostician task store contract ──
178
- describe('INVARIANT: Diagnostician task store', () => {
179
- it('MUST persist tasks with correct structure', async () => {
180
- const taskId = `task-${Date.now()}`;
181
- const prompt = 'Diagnose the following pain signal:\n- source: tool_failure\n- score: 70';
182
-
183
- await addDiagnosticianTask(stateDir, taskId, prompt);
184
-
185
- // Independent verification: read file directly
186
- const tasksPath = path.join(stateDir, 'diagnostician_tasks.json');
187
- expect(fs.existsSync(tasksPath)).toBe(true);
188
-
189
- const store = JSON.parse(fs.readFileSync(tasksPath, 'utf-8'));
190
-
191
- // INVARIANT: Task MUST be in store with correct structure
192
- expect(store.tasks).toBeDefined();
193
- expect(store.tasks[taskId]).toBeDefined();
194
- expect(store.tasks[taskId].prompt).toBe(prompt);
195
- expect(store.tasks[taskId].status).toBe('pending');
196
- expect(store.tasks[taskId].createdAt).toBeDefined();
197
- });
198
-
199
- it('getPendingDiagnosticianTasks MUST only return pending tasks', async () => {
200
- // Add pending task
201
- await addDiagnosticianTask(stateDir, 'pending-task', 'Test prompt');
202
-
203
- // Add completed task manually
204
- const tasksPath = path.join(stateDir, 'diagnostician_tasks.json');
205
- const store = { tasks: {} };
206
- store.tasks['completed-task'] = {
207
- prompt: 'Completed',
208
- status: 'completed',
209
- createdAt: new Date().toISOString(),
210
- };
211
- fs.writeFileSync(tasksPath, JSON.stringify(store));
212
-
213
- // Add pending task to the store
214
- const existingStore = JSON.parse(fs.readFileSync(tasksPath, 'utf-8'));
215
- existingStore.tasks['pending-task'] = {
216
- prompt: 'Pending',
217
- status: 'pending',
218
- createdAt: new Date().toISOString(),
219
- };
220
- fs.writeFileSync(tasksPath, JSON.stringify(existingStore));
221
-
222
- const pending = getPendingDiagnosticianTasks(stateDir);
223
-
224
- // INVARIANT: Only pending tasks MUST be returned
225
- expect(pending.some(t => t.id === 'pending-task')).toBe(true);
226
- expect(pending.some(t => t.id === 'completed-task')).toBe(false);
227
- });
228
- });
229
- });
230
-
231
- // ─────────────────────────────────────────────────────────────────────
232
- // PART 2: Resilience Tests
233
- // Tests that verify system behavior under abnormal conditions
234
- // ─────────────────────────────────────────────────────────────────────
235
-
236
- describe('Pain → Diagnostician: Resilience', () => {
237
- let workspaceDir: string;
238
- let stateDir: string;
239
-
240
- beforeEach(() => {
241
- const ws = createTestWorkspace();
242
- workspaceDir = ws.workspaceDir;
243
- stateDir = ws.stateDir;
244
- });
245
-
246
- afterEach(() => {
247
- cleanupWorkspace(workspaceDir);
248
- });
249
-
250
- // ── RESILIENCE 1: Corruption recovery ──
251
- describe('RESILIENCE: Corruption recovery', () => {
252
- it('readPainFlagData MUST NOT crash on corrupted file', () => {
253
- // Write corrupted content
254
- fs.writeFileSync(path.join(stateDir, '.pain_flag'), 'invalid {{{ json');
255
-
256
- // This should NOT throw
257
- expect(() => readPainFlagData(workspaceDir)).not.toThrow();
258
-
259
- const data = readPainFlagData(workspaceDir);
260
-
261
- // INVARIANT: Should return safe default, not undefined/null
262
- expect(data).toBeDefined();
263
- expect(typeof data).toBe('object');
264
- });
265
-
266
- it('getPendingDiagnosticianTasks MUST NOT crash on missing file', () => {
267
- // Don't create diagnostician_tasks.json
268
-
269
- // This should NOT throw
270
- expect(() => getPendingDiagnosticianTasks(stateDir)).not.toThrow();
271
-
272
- const pending = getPendingDiagnosticianTasks(stateDir);
273
-
274
- // INVARIANT: Should return empty array, not crash
275
- expect(Array.isArray(pending)).toBe(true);
276
- expect(pending.length).toBe(0);
277
- });
278
-
279
- it('getPendingDiagnosticianTasks MUST NOT crash on corrupted JSON', () => {
280
- fs.writeFileSync(
281
- path.join(stateDir, 'diagnostician_tasks.json'),
282
- 'not valid json {{{'
283
- );
284
-
285
- // This should NOT throw
286
- expect(() => getPendingDiagnosticianTasks(stateDir)).not.toThrow();
287
-
288
- const pending = getPendingDiagnosticianTasks(stateDir);
289
-
290
- // INVARIANT: Should return empty array as fallback
291
- expect(Array.isArray(pending)).toBe(true);
292
- });
293
- });
294
-
295
- // ── RESILIENCE 2: Concurrent access safety ──
296
- describe('RESILIENCE: Concurrent access', () => {
297
- it('writePainFlag MUST handle rapid sequential writes', () => {
298
- // Simulate rapid consecutive writes
299
- for (let i = 0; i < 10; i++) {
300
- const data = buildPainFlag({
301
- source: `test-${i}`,
302
- score: String(i * 10),
303
- reason: `Test ${i}`,
304
- });
305
- writePainFlag(workspaceDir, data);
306
- }
307
-
308
- // INVARIANT: File must exist and be valid after rapid writes
309
- const painFlagPath = path.join(stateDir, '.pain_flag');
310
- expect(fs.existsSync(painFlagPath)).toBe(true);
311
-
312
- const content = fs.readFileSync(painFlagPath, 'utf-8');
313
-
314
- // Should not have corruption artifacts
315
- expect(content).not.toContain('undefined');
316
- expect(content).not.toContain('[object Object]');
317
- expect(content).not.toContain('null');
318
- });
319
- });
320
- });
321
-
322
- // ─────────────────────────────────────────────────────────────────────
323
- // PART 3: Round-trip Tests
324
- // Tests that verify data survives the full write → read cycle
325
- // ─────────────────────────────────────────────────────────────────────
326
-
327
- describe('Pain → Diagnostician: Round-trip', () => {
328
- let workspaceDir: string;
329
- let stateDir: string;
330
-
331
- beforeEach(() => {
332
- const ws = createTestWorkspace();
333
- workspaceDir = ws.workspaceDir;
334
- stateDir = ws.stateDir;
335
- });
336
-
337
- afterEach(() => {
338
- cleanupWorkspace(workspaceDir);
339
- });
340
-
341
- it('Pain flag round-trip: write → read → verify', () => {
342
- const original = buildPainFlag({
343
- source: 'tool_failure',
344
- score: '75',
345
- reason: 'npm test failed with exit code 1',
346
- session_id: 'session-abc123',
347
- agent_id: 'main',
348
- is_risky: false,
349
- trace_id: 'trace-xyz789',
350
- trigger_text_preview: 'npm test',
351
- });
352
-
353
- writePainFlag(workspaceDir, original);
354
- const read = readPainFlagData(workspaceDir);
355
-
356
- // INVARIANT: All fields MUST survive round-trip
357
- expect(read.source).toBe(original.source);
358
- expect(read.score).toBe(original.score);
359
- expect(read.reason).toBe(original.reason);
360
- expect(read.session_id).toBe(original.session_id);
361
- expect(read.agent_id).toBe(original.agent_id);
362
- expect(read.is_risky).toBe(original.is_risky);
363
- expect(read.trace_id).toBe(original.trace_id);
364
- expect(read.trigger_text_preview).toBe(original.trigger_text_preview);
365
- });
366
-
367
- it('Diagnostician task round-trip: add → get → verify', async () => {
368
- const taskId = 'round-trip-task';
369
- const prompt = 'Analyze the following error:\n```\nError: ENOENT\n```';
370
-
371
- await addDiagnosticianTask(stateDir, taskId, prompt);
372
- const pending = getPendingDiagnosticianTasks(stateDir);
373
- const task = pending.find(t => t.id === taskId);
374
-
375
- // INVARIANT: Task MUST survive round-trip
376
- expect(task).toBeDefined();
377
- expect(task!.task.prompt).toBe(prompt);
378
- expect(task!.task.status).toBe('pending');
379
- });
380
- });
@@ -1,121 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import { ControlUiQueryService } from '../../src/service/control-ui-query-service.js';
3
-
4
- const mocks = vi.hoisted(() => ({
5
- fromHookContext: vi.fn(),
6
- clearCache: vi.fn(),
7
- controlUiDb: {
8
- all: vi.fn(),
9
- get: vi.fn(),
10
- dispose: vi.fn(),
11
- },
12
- trajectory: {
13
- getDataStats: vi.fn(),
14
- },
15
- }));
16
-
17
- vi.mock('../../src/core/workspace-context.js', () => ({
18
- WorkspaceContext: {
19
- fromHookContext: mocks.fromHookContext,
20
- clearCache: mocks.clearCache,
21
- },
22
- }));
23
-
24
- vi.mock('../../src/core/control-ui-db.js', () => ({
25
- ControlUiDatabase: class {
26
- constructor() {
27
- return mocks.controlUiDb as any;
28
- }
29
- },
30
- }));
31
-
32
- describe('ControlUiQueryService', () => {
33
- afterEach(() => {
34
- vi.clearAllMocks();
35
- });
36
-
37
- it('returns null for unknown thinking models', () => {
38
- mocks.fromHookContext.mockReturnValue({ trajectory: mocks.trajectory } as any);
39
- const service = new ControlUiQueryService('/mock/workspace');
40
-
41
- expect(service.getThinkingModelDetail('UNKNOWN')).toBeNull();
42
- service.dispose();
43
- });
44
-
45
- it('labels overview responses as trajectory analytics rather than runtime control state', () => {
46
- mocks.fromHookContext.mockReturnValue({ trajectory: mocks.trajectory } as any);
47
- mocks.trajectory.getDataStats.mockReturnValue({
48
- dbPath: '/mock/trajectory.db',
49
- dbSizeBytes: 0,
50
- assistantTurns: 0,
51
- userTurns: 0,
52
- toolCalls: 0,
53
- painEvents: 0,
54
- pendingSamples: 0,
55
- approvedSamples: 0,
56
- blobBytes: 0,
57
- lastIngestAt: null,
58
- });
59
- mocks.controlUiDb.get.mockImplementation((sql: string) => {
60
- if (sql.includes('FROM gate_blocks')) return { count: 0 };
61
- if (sql.includes('FROM task_outcomes')) return { count: 0 };
62
- return { count: 0 };
63
- });
64
- mocks.controlUiDb.all.mockImplementation((sql: string) => {
65
- if (sql.includes('v_error_clusters')) return [];
66
- if (sql.includes('v_sample_queue')) return [];
67
- if (sql.includes('correction_samples')) return [];
68
- if (sql.includes('v_thinking_model_effectiveness')) return [];
69
- if (sql.includes('v_thinking_model_daily_trend')) return [];
70
- if (sql.includes('v_thinking_model_scenarios')) return [];
71
- return [];
72
- });
73
- const service = new ControlUiQueryService('/mock/workspace');
74
- const overview = service.getOverview();
75
-
76
- expect(overview.dataSource).toBe('trajectory_db_analytics');
77
- expect(overview.runtimeControlPlaneSource).toBe('pd_evolution_status');
78
- expect(overview.summary.gateBlocks).toBe(0);
79
- expect(overview.summary.taskOutcomes).toBe(0);
80
- service.dispose();
81
- });
82
-
83
- it('surfaces gate block and task outcome counts from trajectory analytics', () => {
84
- mocks.fromHookContext.mockReturnValue({ trajectory: mocks.trajectory } as any);
85
- mocks.trajectory.getDataStats.mockReturnValue({
86
- dbPath: '/mock/trajectory.db',
87
- dbSizeBytes: 0,
88
- assistantTurns: 0,
89
- userTurns: 0,
90
- toolCalls: 0,
91
- painEvents: 0,
92
- pendingSamples: 0,
93
- approvedSamples: 0,
94
- blobBytes: 0,
95
- lastIngestAt: null,
96
- });
97
- mocks.controlUiDb.get.mockImplementation((sql: string) => {
98
- if (sql.includes('FROM gate_blocks')) return { count: 1 };
99
- if (sql.includes('FROM task_outcomes')) return { count: 1 };
100
- return { count: 0 };
101
- });
102
- mocks.controlUiDb.all.mockImplementation((sql: string) => {
103
- if (sql.includes('v_error_clusters')) return [];
104
- if (sql.includes('v_sample_queue')) return [];
105
- if (sql.includes('correction_samples')) return [];
106
- if (sql.includes('v_thinking_model_effectiveness')) return [];
107
- if (sql.includes('v_thinking_model_daily_trend')) return [];
108
- if (sql.includes('v_thinking_model_scenarios')) return [];
109
- return [];
110
- });
111
- const service = new ControlUiQueryService('/mock/workspace');
112
- const overview = service.getOverview();
113
-
114
- expect(overview.summary.gateBlocks).toBe(1);
115
- expect(overview.summary.taskOutcomes).toBe(1);
116
- expect(overview.dataSource).toBe('trajectory_db_analytics');
117
- expect(overview.runtimeControlPlaneSource).toBe('pd_evolution_status');
118
-
119
- service.dispose();
120
- });
121
- });
@@ -1,164 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import * as os from 'os';
5
-
6
- import { recordPersistentFailure, resetFailureState, isTaskKindInCooldown } from '../../src/service/cooldown-strategy.js';
7
- import { readState } from '../../src/service/nocturnal-runtime.js';
8
-
9
- let tmpDir: string;
10
-
11
- beforeEach(() => {
12
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cooldown-test-'));
13
- });
14
-
15
- afterEach(() => {
16
- fs.rmSync(tmpDir, { recursive: true, force: true });
17
- });
18
-
19
- describe('cooldown-strategy', () => {
20
- describe('recordPersistentFailure', () => {
21
- it('sets Tier 1 (30min) on first call', async () => {
22
- await recordPersistentFailure(tmpDir, 'sleep_reflection');
23
- const state = await readState(tmpDir);
24
-
25
- expect(state.taskFailureState).toBeDefined();
26
- expect(state.taskFailureState!['sleep_reflection'].consecutiveFailures).toBe(1);
27
- expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(1);
28
-
29
- const cooldownUntil = new Date(state.taskFailureState!['sleep_reflection'].cooldownUntil!).getTime();
30
- const expectedEnd = Date.now() + 30 * 60 * 1000;
31
- expect(Math.abs(cooldownUntil - expectedEnd)).toBeLessThan(5000);
32
- });
33
-
34
- it('sets Tier 2 (4h) on second call', async () => {
35
- await recordPersistentFailure(tmpDir, 'sleep_reflection');
36
- await recordPersistentFailure(tmpDir, 'sleep_reflection');
37
- const state = await readState(tmpDir);
38
-
39
- expect(state.taskFailureState!['sleep_reflection'].consecutiveFailures).toBe(2);
40
- expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(2);
41
-
42
- const cooldownUntil = new Date(state.taskFailureState!['sleep_reflection'].cooldownUntil!).getTime();
43
- const expectedEnd = Date.now() + 4 * 60 * 60 * 1000;
44
- expect(Math.abs(cooldownUntil - expectedEnd)).toBeLessThan(5000);
45
- });
46
-
47
- it('caps at Tier 3 (24h) on fourth+ call', async () => {
48
- for (let i = 0; i < 4; i++) {
49
- await recordPersistentFailure(tmpDir, 'sleep_reflection');
50
- }
51
- const state = await readState(tmpDir);
52
-
53
- expect(state.taskFailureState!['sleep_reflection'].consecutiveFailures).toBe(4);
54
- expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(3);
55
-
56
- const cooldownUntil = new Date(state.taskFailureState!['sleep_reflection'].cooldownUntil!).getTime();
57
- const expectedEnd = Date.now() + 24 * 60 * 60 * 1000;
58
- expect(Math.abs(cooldownUntil - expectedEnd)).toBeLessThan(5000);
59
- });
60
-
61
- it('independent state per task kind', async () => {
62
- await recordPersistentFailure(tmpDir, 'sleep_reflection');
63
- await new Promise((r) => setTimeout(r, 10)); // ensure distinct timestamps
64
- await recordPersistentFailure(tmpDir, 'keyword_optimization');
65
- const state = await readState(tmpDir);
66
-
67
- expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(1);
68
- expect(state.taskFailureState!['keyword_optimization'].escalationTier).toBe(1);
69
- expect(state.taskFailureState!['sleep_reflection'].cooldownUntil).not.toBe(
70
- state.taskFailureState!['keyword_optimization'].cooldownUntil,
71
- );
72
- });
73
- });
74
-
75
- describe('resetFailureState', () => {
76
- it('clears failures after escalation', async () => {
77
- await recordPersistentFailure(tmpDir, 'sleep_reflection');
78
- await resetFailureState(tmpDir, 'sleep_reflection');
79
- const state = await readState(tmpDir);
80
-
81
- expect(state.taskFailureState!['sleep_reflection'].consecutiveFailures).toBe(0);
82
- expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(0);
83
- expect(state.taskFailureState!['sleep_reflection'].cooldownUntil).toBeUndefined();
84
- });
85
-
86
- it('is idempotent with no prior state', async () => {
87
- await expect(resetFailureState(tmpDir, 'sleep_reflection')).resolves.not.toThrow();
88
- });
89
- });
90
-
91
- describe('isTaskKindInCooldown', () => {
92
- it('returns inCooldown=true after recordPersistentFailure', async () => {
93
- await recordPersistentFailure(tmpDir, 'sleep_reflection');
94
- const result = isTaskKindInCooldown(tmpDir, 'sleep_reflection');
95
-
96
- expect(result.inCooldown).toBe(true);
97
- expect(result.remainingMs).toBeGreaterThan(0);
98
- expect(result.cooldownUntil).not.toBeNull();
99
- });
100
-
101
- it('returns inCooldown=false when cooldownUntil is in the past', async () => {
102
- // Manually set expired cooldown
103
- const state = await readState(tmpDir);
104
- state.taskFailureState = {
105
- sleep_reflection: {
106
- consecutiveFailures: 1,
107
- escalationTier: 1,
108
- cooldownUntil: new Date(Date.now() - 60000).toISOString(),
109
- },
110
- };
111
- const { writeState } = await import('../../src/service/nocturnal-runtime.js');
112
- await writeState(tmpDir, state);
113
-
114
- const result = isTaskKindInCooldown(tmpDir, 'sleep_reflection');
115
- expect(result.inCooldown).toBe(false);
116
- expect(result.remainingMs).toBe(0);
117
- });
118
-
119
- it('returns inCooldown=false when no state exists', () => {
120
- const result = isTaskKindInCooldown(tmpDir, 'sleep_reflection');
121
- expect(result.inCooldown).toBe(false);
122
- expect(result.remainingMs).toBe(0);
123
- });
124
-
125
- it('returns inCooldown=false for untracked task kind', async () => {
126
- await recordPersistentFailure(tmpDir, 'sleep_reflection');
127
- const result = isTaskKindInCooldown(tmpDir, 'keyword_optimization');
128
-
129
- expect(result.inCooldown).toBe(false);
130
- expect(result.remainingMs).toBe(0);
131
- });
132
- });
133
-
134
- describe('state persistence', () => {
135
- it('state survives to disk after recordPersistentFailure', async () => {
136
- await recordPersistentFailure(tmpDir, 'sleep_reflection');
137
-
138
- // Read directly from file to verify persistence
139
- const filePath = path.join(tmpDir, 'nocturnal-runtime.json');
140
- expect(fs.existsSync(filePath)).toBe(true);
141
- const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
142
- expect(raw.taskFailureState).toBeDefined();
143
- expect(raw.taskFailureState.sleep_reflection).toBeDefined();
144
- expect(raw.taskFailureState.sleep_reflection.consecutiveFailures).toBe(1);
145
- });
146
- });
147
-
148
- describe('custom config', () => {
149
- it('uses custom tier durations', async () => {
150
- const customConfig = {
151
- tier1_ms: 1000,
152
- tier2_ms: 5000,
153
- tier3_ms: 10000,
154
- consecutive_threshold: 3,
155
- };
156
- await recordPersistentFailure(tmpDir, 'sleep_reflection', customConfig);
157
- const state = await readState(tmpDir);
158
-
159
- const cooldownUntil = new Date(state.taskFailureState!['sleep_reflection'].cooldownUntil!).getTime();
160
- const expectedEnd = Date.now() + 1000;
161
- expect(Math.abs(cooldownUntil - expectedEnd)).toBeLessThan(1000);
162
- });
163
- });
164
- });